UNPKG

@stackbit/utils

Version:
1,051 lines (983 loc) 45.4 kB
import _ from 'lodash'; import { getLocalizedFieldForLocale } from '@stackbit/types'; import type { Model, Field, FieldListItems, FieldListProps, FieldObjectProps, FieldPath, Document, DocumentWithSource, DocumentField, DocumentFieldNonLocalized, DocumentListFieldItems } from '@stackbit/types'; import { getDocumentFieldValue } from './document-utils'; /** * Converts a FieldPath into a string. * Puts complex strings inside single quotes '', * and uses square brackets [] for number keys. */ export function fieldPathToString(fieldPath: FieldPath) { return _.reduce( fieldPath, (accumulator, fieldName, index) => { if (_.isString(fieldName) && /\W/.test(fieldName)) { // field name is a string with non-alphanumeric character accumulator += `['${fieldName}']`; } else if (_.isNumber(fieldName)) { accumulator += `[${fieldName}]`; } else { if (index > 0) { accumulator += '.'; } accumulator += fieldName; } return accumulator; }, '' ); } /** * Converts string into FieldPath * * ```js * stringToFieldPath("sections[0].button['button-label']") * => * ["sections", 0, "button", "button-label"] * ``` */ export function stringToFieldPath(fieldPath: string): FieldPath { const re = /\.|\[([^\]]+)\]\.?/g; const result = []; let match: RegExpExecArray | null = null; let prevLastIndex = 0; while ((match = re.exec(fieldPath)) !== null) { if (prevLastIndex < match.index) { result.push(fieldPath.substring(prevLastIndex, match.index)); } if (match[1]) { // matched [...] if (/^\d+$/.test(match[1])) { result.push(parseInt(match[1])); } else { // remove quotes inside brackets // "hero['field-name'][2]" => ["hero", "field-name", 2] result.push(match[1].replace(/^'(.*)'$/, '$1').replace(/^"(.*)"$/, '$1')); } } prevLastIndex = re.lastIndex; } if (prevLastIndex < fieldPath.length) { result.push(fieldPath.substring(prevLastIndex)); } return result; } export function getModelFieldAtFieldPath({ document, fieldPath, modelMap, locale }: { document: Document; fieldPath: (string | number)[]; modelMap: Record<string, Model>; locale?: string; }): Field | FieldListItems { if (_.isEmpty(fieldPath)) { throw new Error('the fieldPath can not be empty'); } const model = modelMap[document.modelName]; if (!model) { throw new Error(`model with name '${document.modelName}' of a document '${document.id}' not found`); } function getField(docField: DocumentField, modelField: Field | FieldListItems, fieldPath: (string | number)[]): Field | FieldListItems { const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error('the first fieldPath item must be string'); } const childFieldPath = _.tail(fieldPath); let childDocField: DocumentField | undefined; let childModelField: Field | undefined; switch (docField.type) { case 'object': { const localizedObjectField = getLocalizedFieldForLocale(docField, locale); if (!localizedObjectField) { throw new Error(`locale for field was not found`); } childDocField = localizedObjectField.fields[fieldName]; childModelField = _.find((modelField as FieldObjectProps).fields, (field) => field.name === fieldName); if (!childDocField || !childModelField) { throw new Error(`field ${fieldName} doesn't exist`); } if (childFieldPath.length === 0) { return childModelField; } return getField(childDocField, childModelField, childFieldPath); } case 'model': { const localizedModelField = getLocalizedFieldForLocale(docField, locale); if (!localizedModelField) { throw new Error(`locale for field was not found`); } const modelName = localizedModelField.modelName; const childModel = modelMap[modelName]; if (!childModel) { throw new Error(`model ${modelName} doesn't exist`); } childModelField = _.find(childModel.fields, (field) => field.name === fieldName); childDocField = localizedModelField.fields![fieldName]; if (!childDocField || !childModelField) { throw new Error(`field ${fieldName} doesn't exist`); } if (childFieldPath.length === 0) { return childModelField; } return getField(childDocField, childModelField!, childFieldPath); } case 'list': { const localizedListField = getLocalizedFieldForLocale(docField, locale); if (!localizedListField) { throw new Error(`locale for field was not found`); } const listItem = localizedListField.items && localizedListField.items[fieldName as number]; const listItemsModel = (modelField as FieldListProps).items; if (!listItem || !listItemsModel) { throw new Error(`field ${fieldName} doesn't exist`); } if (childFieldPath.length === 0) { return listItemsModel; } if (!Array.isArray(listItemsModel)) { return getField(listItem, listItemsModel, childFieldPath); } else { const fieldListItems = (listItemsModel as FieldListItems[]).find((listItemsModel) => listItemsModel.type === listItem.type); if (!fieldListItems) { throw new Error('cannot find matching field model'); } return getField(listItem, fieldListItems, childFieldPath); } } default: if (!_.isEmpty(childFieldPath)) { throw new Error('illegal fieldPath'); } return modelField; } } const fieldName = _.head(fieldPath); const childFieldPath = _.tail(fieldPath); if (typeof fieldName !== 'string') { throw new Error('the first fieldPath item must be string'); } const childDocField = document.fields[fieldName]; const childModelField = _.find(model.fields, { name: fieldName }); if (!childDocField || !childModelField) { throw new Error(`field ${fieldName} doesn't exist`); } if (childFieldPath.length === 0) { return childModelField; } return getField(childDocField, childModelField, childFieldPath); } export type GetDocumentFieldAtFieldPathOptions = { document: Document; fieldPath: FieldPath; locale?: string; isFullFieldPath?: boolean; returnUndefined?: boolean; }; export type GetDocumentFieldAtFieldPathOptionsThrow = GetDocumentFieldAtFieldPathOptions & { returnUndefined?: false; }; export type GetDocumentFieldAtFieldPathOptionsUndefined = GetDocumentFieldAtFieldPathOptions & { returnUndefined: true; }; /** * This function receives a `document` and returns DocumentFieldNonLocalized at * the specified `fieldPath` while resolving any localized fields with the * specified `locale`. * * ```ts * getDocumentFieldAtFieldPath({ * document, * locale, * fieldPath: ['sections', 1, 'title'] * }) * ``` * * For improved localization support, use the getModelAndDocumentFieldForLocalizedFieldPath * method instead. * * The `isFullFieldPath` flag specifies if the `fieldPath` includes container * specifiers such as "fields" and "items": * * ```ts * getDocumentFieldAtFieldPath({ * document, * isFullFieldPath: false, * fieldPath: ['sections', 1, 'title'] * }) * getDocumentFieldAtFieldPath({ * document, * isFullFieldPath: true, * fieldPath: ['fields', 'sections', 'items', 1, 'fields', 'title'] * }) * ``` * * By default, if the document field at the specified `fieldPath` not found, this * function will throw exception. Setting `returnUndefined` to true will allow * the function to return `undefined` if the document field is not found. */ export function getDocumentFieldAtFieldPath(options: GetDocumentFieldAtFieldPathOptionsThrow): DocumentFieldNonLocalized; export function getDocumentFieldAtFieldPath(options: GetDocumentFieldAtFieldPathOptionsUndefined): DocumentFieldNonLocalized | undefined; export function getDocumentFieldAtFieldPath({ document, fieldPath, locale, isFullFieldPath, returnUndefined }: GetDocumentFieldAtFieldPathOptions): DocumentFieldNonLocalized | undefined { function throwOrReturnUndefined(errorMessage: string) { if (returnUndefined) { return undefined; } else { throw new Error(errorMessage); } } const origFieldPath = fieldPath; const origFieldPathStr = fieldPath.join('.'); if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error(`Illegal fieldPath '${origFieldPathStr}'. The fieldPath must start with "fields" specifier when 'isFullFieldPath' is set.`); } fieldPath = _.tail(fieldPath); } if (_.isEmpty(fieldPath)) { throw new Error(`Illegal fieldPath '${origFieldPathStr}'. The fieldPath cannot be empty`); } const docId = document.id; const modelName = document.modelName; function getPrefixOf(fieldPath: FieldPath, include = 0) { return origFieldPath.slice(0, origFieldPath.length - fieldPath.length + include).join('.'); } function getField(docField: DocumentField | DocumentListFieldItems, fieldPath: FieldPath): DocumentFieldNonLocalized | undefined { const localizedField = getLocalizedFieldForLocale(docField, locale); if (!localizedField) { return throwOrReturnUndefined( `Illegal fieldPath '${origFieldPathStr}'. The value of a field at path '${getPrefixOf(fieldPath)}' ` + `of a document '${docId}' (model: '${modelName}') for the '${locale}' locale is undefined.` ); } // if no more items in fieldPath return the found document and model fields if (fieldPath.length === 0) { return localizedField; } switch (localizedField.type) { case 'object': case 'model': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is an object field and must be followed by the "fields" specifier when 'isFullFieldPath' is set.` ); } fieldPath = _.tail(fieldPath); } const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `of a document '${docId}' (model: '${modelName}') is an object field and must be followed by a field name.` ); } fieldPath = _.tail(fieldPath); const childDocField = localizedField.fields[fieldName]; if (!childDocField) { return throwOrReturnUndefined( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `of a document '${docId}' (model: '${modelName}') points to a non existing field.` ); } return getField(childDocField, fieldPath); } case 'list': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'items') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a list field and must be followed by the "items" specifier when 'isFullFieldPath' is set.` ); } fieldPath = _.tail(fieldPath); } const itemIndex = _.head(fieldPath) as number; if (typeof itemIndex === 'undefined') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `of a document '${docId}' (model: '${modelName}') is a list field and must be followed by a list item index.` ); } fieldPath = _.tail(fieldPath); const listItem = localizedField.items && localizedField.items[itemIndex]; if (!listItem) { return throwOrReturnUndefined( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `of a document '${docId}' (model: '${modelName}') points to a non existing list item.` ); } return getField(listItem, fieldPath); } default: { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a primitive field of type '${docField.type}' and cannot be followed by another field path.` ); } } } const fieldName = _.head(fieldPath); const restFieldPath = _.tail(fieldPath); if (typeof fieldName !== 'string') { return throwOrReturnUndefined('the first fieldPath item must be string'); } const childDocField: DocumentField | undefined = document.fields[fieldName]; if (!childDocField) { return throwOrReturnUndefined(`document '${docId}' of type '${modelName}' doesn't have a field named '${fieldName}'`); } return getField(childDocField, restFieldPath); } /** * This function receives a `document` and a `modelMap` and returns an object * with DocumentFieldNonLocalized and a model Field at the specified `fieldPath` * while resolving any localized fields with the specified `locale`. * * @example * getDocumentAndModelFieldAtFieldPath({ * document, * locale, * modelMap, * fieldPath: ['sections', 1, 'title'] * }) * * For improved localization support, use the getModelAndDocumentFieldForLocalizedFieldPath * method instead. * * The `isFullFieldPath` flag specifies if the `fieldPath` includes container * specifiers such as "fields" and "items". * * @example * isFullFieldPath: false => fieldPath: ['sections', 1, 'title'] * isFullFieldPath: true => fieldPath: ['fields', 'sections', 'items', 1, 'fields', 'title'] */ export function getDocumentAndModelFieldAtFieldPath({ document, fieldPath, modelMap, locale, isFullFieldPath }: { document: Document; fieldPath: (string | number)[]; modelMap: Record<string, Model>; locale?: string; isFullFieldPath?: boolean; }): { modelField: Field | FieldListItems; documentField: DocumentFieldNonLocalized } { if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error('fieldPath must start with "fields" specifier'); } fieldPath = _.tail(fieldPath); } if (_.isEmpty(fieldPath)) { throw new Error('the fieldPath cannot be empty'); } const docId = document.id; const docModelName = document.modelName; const origFieldPath = fieldPath; const origFieldPathStr = fieldPath.join('.'); let parentModel: Model = modelMap[docModelName]!; let fieldPathFromParentModel = fieldPath; if (!parentModel) { throw new Error(`model with name '${docModelName}' of a document '${docId}' not found`); } function getPrefixOf(fieldPath: (string | number)[], include = 0) { return origFieldPath.slice(0, origFieldPath.length - fieldPath.length + include).join('.'); } function getModelPrefixOf(fieldPath: (string | number)[], include = 0) { return fieldPathFromParentModel.slice(0, fieldPathFromParentModel.length - fieldPath.length + include).join('.'); } function getField( docField: DocumentField | DocumentListFieldItems, modelField: Field | FieldListItems, fieldPath: (string | number)[] ): { modelField: Field | FieldListItems; documentField: DocumentFieldNonLocalized } { const localizedField = getLocalizedFieldForLocale(docField, locale); if (!localizedField) { throw new Error( `the value of a field at fieldPath '${getPrefixOf(fieldPath)}' is undefined ` + `for the document '${docId}' of type '${docModelName}' for the '${locale}' locale.` ); } // if no more items in fieldPath return the found document and model fields if (fieldPath.length === 0) { return { modelField: modelField, documentField: localizedField }; } switch (localizedField.type) { case 'object': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error(`the fieldPath '${getPrefixOf(fieldPath, 1)}' must contain "fields" specifier before a field's name`); } fieldPath = _.tail(fieldPath); } const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error( `the fieldPath '${getPrefixOf(fieldPath, 1)}' of a document '${docId}' of type '${docModelName}' must point to a field name` ); } fieldPath = _.tail(fieldPath); const childDocField = localizedField.fields[fieldName]; if (!childDocField) { throw new Error( `the fieldPath '${getPrefixOf(fieldPath)}' points to a non existing field in a document '${docId}' of type '${docModelName}'` ); } if (modelField.type !== 'object') { throw new Error( `model field of type '${modelField.type}' of model '${parentModel.name}' at field path '${getModelPrefixOf(fieldPath)}' ` + `doesn't match document field of type 'object' of document '${docId}' at field path '${getPrefixOf(fieldPath)}'` ); } const childModelField = _.find(modelField.fields, (field) => field.name === fieldName); if (!childModelField) { throw new Error(`model '${parentModel.name}' doesn't have a field at path: '${getModelPrefixOf(fieldPath)}'`); } return getField(childDocField, childModelField, fieldPath); } case 'model': { fieldPathFromParentModel = fieldPath; if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error(`the fieldPath '${getPrefixOf(fieldPath, 1)}' must contain "fields" specifier before a field's name`); } fieldPath = _.tail(fieldPath); } const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error( `the fieldPath '${getPrefixOf(fieldPath, 1)}' of a document '${docId}' of type '${docModelName}' must point to a field name` ); } fieldPath = _.tail(fieldPath); const childDocField = localizedField.fields[fieldName]; if (!childDocField) { throw new Error( `the fieldPath '${getPrefixOf(fieldPath)}' points to a non existing field in a document '${docId}' of type '${docModelName}'` ); } const modelName = localizedField.modelName; const childModel = modelMap[modelName]; if (!childModel) { throw new Error( `the "model" field at path '${getPrefixOf(fieldPath)}' of a document '${docId}' of type '${docModelName}' ` + `contains an object with type of non existing model '${modelName}'` ); } parentModel = childModel; const childModelField = _.find(childModel.fields, (field) => field.name === fieldName); if (!childModelField) { throw new Error(`model '${childModel.name}' doesn't have a field at path: '${getModelPrefixOf(fieldPath)}'`); } return getField(childDocField, childModelField, fieldPath); } case 'list': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'items') { throw new Error(`the fieldPath '${getPrefixOf(fieldPath, 1)}' must contain "items" specifier before a list item index`); } fieldPath = _.tail(fieldPath); } const itemIndex = _.head(fieldPath) as number; if (typeof itemIndex === 'undefined') { throw new Error( `the fieldPath '${getPrefixOf(fieldPath, 1)}' of a document '${docId}' of type '${docModelName}' must point to a list item index` ); } fieldPath = _.tail(fieldPath); const listItem = localizedField.items && localizedField.items[itemIndex]; if (!listItem) { throw new Error( `the fieldPath '${getPrefixOf(fieldPath)}' points to a non existing list item in a document '${docId}' of type '${docModelName}'` ); } if (modelField.type !== 'list') { throw new Error( `model field of type '${modelField.type}' of model '${parentModel.name}' at field path '${getModelPrefixOf(fieldPath)}' ` + `doesn't match document field of type 'list' of document '${docId}' at field path '${getPrefixOf(fieldPath)}'` ); } const listItemsModel = modelField.items; if (!Array.isArray(listItemsModel)) { return getField(listItem, listItemsModel, fieldPath); } else { const fieldListItems = (listItemsModel as FieldListItems[]).find((listItemsModel) => listItemsModel.type === listItem.type); if (!fieldListItems) { throw new Error( `cannot find list item model for list item of type '${listItem.type}' for model '${parentModel.name}' ` + `at field path '${getModelPrefixOf(fieldPath)}'` ); } return getField(listItem, fieldListItems, fieldPath); } } default: { const primitiveFieldName = origFieldPath[origFieldPath.length - fieldPath.length - 1]; throw new Error( `the fieldPath '${origFieldPathStr}' is illegal, a primitive field '${primitiveFieldName}' of type '${localizedField.type}' cannot be followed by additional field paths` ); } } } const fieldName = _.head(fieldPath); const restFieldPath = _.tail(fieldPath); if (typeof fieldName !== 'string') { throw new Error('the first fieldPath item must be string'); } const childDocField: DocumentField | undefined = document.fields[fieldName]; if (!childDocField) { throw new Error(`document '${docId}' of type '${docModelName}' doesn't have a field named '${fieldName}'`); } const childModelField: Field | undefined = _.find(parentModel.fields, { name: fieldName }); if (!childModelField) { throw new Error(`model '${docModelName}' doesn't have a field named '${fieldName}'`); } return getField(childDocField, childModelField, restFieldPath); } /** * This function receives a `document` and a `modelMap` and returns an object * with DocumentField and a model Field at the specified `fieldPath`. * * If some fields along the fieldPath are localized, the `fieldPath` must * contain the locale of the field under the "locales" property. The locales * along the field path don't have to be the same. * * @example * fieldPath: ['fields', 'button', 'locales', 'en', 'fields', 'title', 'locales', 'es'] * * If the provided `fieldPath` points to a list item, the returned model field * and document field will belong to a list item. In this case, the model field * will contain only field-specific properties and the document field will be * localized. * * @example * fieldPath: ['fields', 'buttons', 'items', 2] * * The `isFullFieldPath` flag specifies if the `fieldPath` includes container * specifiers such as "fields" and "items". * * @example * isFullFieldPath: false => fieldPath: ['sections', 1, 'title', 'es'] * isFullFieldPath: true => fieldPath: ['fields', 'sections', 'items', 1, 'fields', 'title', 'locales', 'es'] */ export function getModelAndDocumentFieldForLocalizedFieldPath({ document, fieldPath, modelMap, isFullFieldPath }: { document: Document; fieldPath: (string | number)[]; modelMap: Record<string, Model>; isFullFieldPath?: boolean; }): { modelField: Field | FieldListItems; documentField?: DocumentField | DocumentListFieldItems } { if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error('fieldPath must start with "fields" specifier'); } fieldPath = _.tail(fieldPath); } if (_.isEmpty(fieldPath)) { throw new Error('the fieldPath cannot be empty'); } const docId = document.id; const origModelName = document.modelName; const origFieldPath = fieldPath; const origFieldPathStr = fieldPath.join('.'); const model = modelMap[origModelName]; if (!model) { throw new Error(`model with name '${origModelName}' of a document '${docId}' not found`); } let parentModel = model; let fieldPathFromParentModel = fieldPath; function getPrefixOf(fieldPath: (string | number)[], include = 0) { return origFieldPath.slice(0, origFieldPath.length - fieldPath.length + include).join('.'); } function getModelPrefixOf(fieldPath: (string | number)[], include = 0) { return fieldPathFromParentModel.slice(0, fieldPathFromParentModel.length - fieldPath.length + include).join('.'); } function getField( docField: DocumentField | DocumentListFieldItems, modelField: Field | FieldListItems, fieldPath: (string | number)[] ): { modelField: Field | FieldListItems; documentField?: DocumentField | DocumentListFieldItems } { // if no more items in fieldPath return the found document and model fields if (fieldPath.length === 0) { return { modelField: modelField, documentField: docField }; } if (docField.localized) { if (isFullFieldPath) { if (_.head(fieldPath) !== 'locales') { throw new Error(`fieldPath '${origFieldPath.join('.')}' must contain "locales" specifier for localized field`); } fieldPath = _.tail(fieldPath); } const locale = _.head(fieldPath); if (typeof locale !== 'string') { throw new Error(`the locale specifier must be string in fieldPath '${origFieldPath.join('.')}'`); } fieldPath = _.tail(fieldPath); const localizedDocField = getLocalizedFieldForLocale(docField, locale); if (!localizedDocField) { if (!fieldPath.length) { // field locale is not set return { modelField, documentField: undefined }; } throw new Error(`fieldPath '${origFieldPath.join('.')}' doesn't specify the locale`); } docField = localizedDocField; } switch (docField.type) { case 'object': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error(`fieldPath '${getPrefixOf(fieldPath, 1)}' must contain "fields" specifier after an "object" field`); } fieldPath = _.tail(fieldPath); } const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error(`fieldPath '${getPrefixOf(fieldPath, 1)}' must specify a field name for an "object" field`); } fieldPath = _.tail(fieldPath); const childDocField = docField.fields[fieldName]; if (!childDocField) { throw new Error(`document '${docId}' of type '${origModelName}' doesn't have a field at path: '${getPrefixOf(fieldPath)}'`); } if (modelField.type !== 'object') { throw new Error( `model field of type '${modelField.type}' of model '${parentModel.name}' at field path '${getModelPrefixOf(fieldPath)}' ` + `doesn't match document field of type 'object' of document '${docId}' at field path '${getPrefixOf(fieldPath)}'` ); } const childModelField = _.find(modelField.fields, (field) => field.name === fieldName); if (!childModelField) { throw new Error(`model '${parentModel.name}' doesn't have a field at path: '${getModelPrefixOf(fieldPath)}'`); } return getField(childDocField, childModelField, fieldPath); } case 'model': { fieldPathFromParentModel = fieldPath; if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error(`fieldPath '${getPrefixOf(fieldPath, 1)}' must contain "fields" specifier after a "model" field`); } fieldPath = _.tail(fieldPath); } const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error(`fieldPath '${getPrefixOf(fieldPath, 1)}' must specify a field name for an "model" field`); } fieldPath = _.tail(fieldPath); const modelName = docField.modelName; const childModel = modelMap[modelName]; if (!childModel) { throw new Error( `a "model" field of a document '${docId}' at path '${getPrefixOf(fieldPath)}' ` + `contains an object with non existing modelName '${modelName}'` ); } parentModel = childModel; const childDocField = docField.fields[fieldName]; if (!childDocField) { throw new Error(`document '${docId}' of type '${origModelName}' doesn't have a field at path: '${getPrefixOf(fieldPath)}'`); } const childModelField = _.find(childModel.fields, (field) => field.name === fieldName); if (!childModelField) { throw new Error(`model '${childModel.name}' doesn't have a field at path: '${getModelPrefixOf(fieldPath)}'`); } return getField(childDocField, childModelField, fieldPath); } case 'list': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'items') { throw new Error(`fieldPath '${getPrefixOf(fieldPath, 1)}' must contain "items" specifier after a "list" field`); } fieldPath = _.tail(fieldPath); } const itemIndex = _.head(fieldPath) as number; if (typeof itemIndex === 'undefined') { throw new Error(`fieldPath '${getPrefixOf(fieldPath, 1)}' must specify a list item index for a "list" field`); } fieldPath = _.tail(fieldPath); const listItem = docField.items && docField.items[itemIndex as number]; if (!listItem) { throw new Error(`document '${docId}' of type '${origModelName}' doesn't have a list item at path: '${getPrefixOf(fieldPath)}'`); } if (modelField.type !== 'list') { throw new Error( `model field type '${modelField.type}' of a model '${parentModel.name}' at field path '${getModelPrefixOf(fieldPath)}' ` + `doesn't match document field of type 'list' of document '${docId}' at field path '${getPrefixOf(fieldPath)}'` ); } const listItemsModel = modelField.items; if (!Array.isArray(listItemsModel)) { return getField(listItem, listItemsModel, fieldPath); } else { const fieldListItems = (listItemsModel as FieldListItems[]).find((listItemsModel) => listItemsModel.type === listItem.type); if (!fieldListItems) { throw new Error( `cannot find list item model for document list item of type '${listItem.type}' for model '${parentModel.name}' ` + `at field path '${getModelPrefixOf(fieldPath, 1)}'` ); } return getField(listItem, fieldListItems, fieldPath); } } default: if (!_.isEmpty(fieldPath)) { throw new Error( `illegal fieldPath, a primitive field of type '${docField.type}' was found in the middle of the fieldPath '${origFieldPathStr}'` ); } return { modelField, documentField: docField }; } } const fieldName = _.head(fieldPath); const childFieldPath = _.tail(fieldPath); if (typeof fieldName !== 'string') { throw new Error('the first fieldPath item must be string'); } const childDocField: DocumentField | undefined = document.fields[fieldName]; const childModelField: Field | undefined = _.find(model.fields, { name: fieldName }); if (!childDocField) { throw new Error(`document '${docId}' of type '${model.name}' doesn't have a field named '${fieldName}'`); } if (!childModelField) { throw new Error(`model '${model.name}' doesn't have a field named '${fieldName}'`); } return getField(childDocField, childModelField, childFieldPath); } /** * This function returns the value of a field in the specified `document` at the * specified `fieldPath`. * * ```ts * getDocumentFieldValueAtFieldPath({ * document, * fieldPath: 'sections[1].title' * }) * ``` * * The `fieldPath` can also contain locale specifiers for localized fields. * * ```ts * getDocumentFieldValueAtFieldPath({ * document, * fieldPath: 'sections[1].title.es' * }) * ``` * * Note: getting a value of a localized field without using a locale specifier * will throw an exception. * * The `fieldPath` can be a string following the standard JavaScript notation * for getting nested properties and array values, or a {@link FieldPath}. * * If the document field at the specified `fieldPath` not found, undefined is returned. * * @param document The parent document * @param fieldPath The field path to the target field * @param getDocumentById A function * @param isFullFieldPath */ export function getDocumentFieldValueAtFieldPath({ document, fieldPath, getDocumentById, isFullFieldPath }: { document: DocumentWithSource; fieldPath: FieldPath | string; getDocumentById?: (options: { id: string; srcType?: string; srcProjectId?: string }) => DocumentWithSource | undefined; isFullFieldPath?: boolean; }): unknown { if (typeof fieldPath === 'string') { fieldPath = stringToFieldPath(fieldPath); } const origFieldPath = fieldPath; const origFieldPathStr = fieldPath.join('.'); if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error(`Illegal fieldPath '${origFieldPathStr}'. The fieldPath must start with "fields" specifier when 'isFullFieldPath' is set.`); } fieldPath = _.tail(fieldPath); } if (_.isEmpty(fieldPath)) { throw new Error(`Illegal fieldPath '${origFieldPathStr}'. The fieldPath cannot be empty`); } function getPrefixOf(fieldPath: FieldPath, include = 0) { return origFieldPath.slice(0, origFieldPath.length - fieldPath.length + include).join('.'); } function getField(docField: DocumentField | DocumentListFieldItems, fieldPath: FieldPath): unknown { if (docField.localized) { if (isFullFieldPath) { if (_.head(fieldPath) !== 'locales') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a localized field and must be followed by the "locales" specifier when 'isFullFieldPath' is set.` ); } fieldPath = _.tail(fieldPath); } const locale = _.head(fieldPath); if (typeof locale !== 'string') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a localized field and must be followed by a locale specifier.` ); } fieldPath = _.tail(fieldPath); const localizedDocField = getLocalizedFieldForLocale(docField, locale); if (!localizedDocField) { return undefined; } docField = localizedDocField; } // if no more items in fieldPath return the found document fields if (fieldPath.length === 0) { return getDocumentFieldValue({ documentField: docField }); } switch (docField.type) { case 'object': case 'model': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a '${docField.type}' field and must be followed by the "fields" specifier when 'isFullFieldPath' is set.` ); } fieldPath = _.tail(fieldPath); } const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is an '${docField.type}' field and must be followed by a field name.` ); } fieldPath = _.tail(fieldPath); const childDocField = docField.fields[fieldName]; if (!childDocField) { return undefined; } return getField(childDocField, fieldPath); } case 'reference': { if (docField.refType !== 'document') { return undefined; } if (!getDocumentById) { throw new Error( `Can't get the field value for a field at the fieldPath '${origFieldPathStr}'. ` + `The getDocumentById() function was not provided, ` + `and the field at path '${getPrefixOf(fieldPath)}' is a reference field.` ); } const refDocument = getDocumentById({ id: docField.refId, srcType: document.srcType, srcProjectId: document.srcProjectId }); if (!refDocument) { return undefined; } if (isFullFieldPath) { if (_.head(fieldPath) !== 'fields') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a reference field and must be followed by the "fields" specifier when 'isFullFieldPath' is set.` ); } fieldPath = _.tail(fieldPath); } const fieldName = _.head(fieldPath); if (typeof fieldName === 'undefined') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a reference field and must be followed by a field name.` ); } fieldPath = _.tail(fieldPath); const childDocField = refDocument.fields[fieldName]; if (!childDocField) { return undefined; } return getField(childDocField, fieldPath); } case 'list': { if (isFullFieldPath) { if (_.head(fieldPath) !== 'items') { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a list field and must be followed by the "items" specifier when 'isFullFieldPath' is set.` ); } fieldPath = _.tail(fieldPath); } const itemIndex = _.head(fieldPath) as number; if (typeof itemIndex === 'undefined' || (typeof itemIndex !== 'number' && !/\d+/.test(itemIndex))) { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a list field and must be followed by a list item index.` ); } fieldPath = _.tail(fieldPath); const listItem = docField.items && docField.items[itemIndex]; if (!listItem) { return; } return getField(listItem, fieldPath); } default: { throw new Error( `Illegal fieldPath '${origFieldPathStr}'. The field at path '${getPrefixOf(fieldPath)}' ` + `is a primitive field of type '${docField.type}' and cannot be followed by another field path.` ); } } } const fieldName = _.head(fieldPath); const childFieldPath = _.tail(fieldPath); if (typeof fieldName !== 'string') { throw new Error(`Illegal fieldPath '${origFieldPathStr}'. The first fieldPath item must be a string specifying a document's field name.`); } const childDocField: DocumentField | undefined = document.fields[fieldName]; if (!childDocField) { return undefined; } return getField(childDocField, childFieldPath); }