@stackbit/utils
Version:
Stackbit utilities
1,051 lines (983 loc) • 45.4 kB
text/typescript
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);
}