aura-enum
Version:
1,759 lines (1,755 loc) • 79.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var auraShared = require('aura-shared');
let EnumUtils$1 = class EnumUtils {
/**
* @description 从配置中获取值
* @private
* @param item - 枚举项
* @param config - 配置项,可以是函数、字符串或字符串数组
* @returns 提取的值
*/
static getValueFromConfig(item, config) {
if (auraShared.isFunction(config)) {
return config(item);
} else if (auraShared.isArray(config)) {
return config.map((key) => item[key]).filter((val) => val !== void 0).join(" ");
}
return item[config];
}
/**
* 创建排序函数
*/
static createSortFunction(field) {
if (typeof field === "function")
return field;
return (a, b) => {
const aVal = a[field];
const bVal = b[field];
if (aVal === void 0 && bVal === void 0)
return 0;
if (aVal === void 0)
return 1;
if (bVal === void 0)
return -1;
return String(aVal).localeCompare(String(bVal));
};
}
};
class EnumFactory {
/**
* @description 构造函数,用于初始化枚举工厂
* @param enumObj - 枚举对象,可以是简单的键值对或复杂对象
* @param options - 配置选项
* @param options.labelKeys - 用于提取标签的键或函数
* @param options.valueKeys - 用于提取值的键或函数
* @param options.key - 用于提取键的键或函数
* @param options.isEnumFactory - 用于提取键的键或函数
*/
constructor(name, enumObj, options = {}) {
this.strictMode = false;
this.defaultLang = "zh";
// 假设默认语言是英语
this.plugins = [];
this.isEnumStore = true;
/**
* 添加自定义验证规则
* @param rules - 验证规则数组
*/
this.validationRules = [];
this.cache = /* @__PURE__ */ new Map();
this.history = [];
this.observers = /* @__PURE__ */ new Set();
this.metadata = /* @__PURE__ */ new Map();
this.indexes = /* @__PURE__ */ new Map();
this.version = 1;
this.migrations = [];
const { labelKeys = ["label"], valueKeys = ["value"], key = "key", isEnumStore = true } = options;
this.name = name;
this.originList = enumObj;
this.isEnumStore = isEnumStore;
const entries = Object.entries(enumObj);
this.keys = [];
this.labels = [];
this.values = [];
this.disposedList = [];
this.entriesMapByKey = /* @__PURE__ */ new Map();
this.entriesMapByLabel = /* @__PURE__ */ new Map();
this.entriesMapByValue = /* @__PURE__ */ new Map();
if (entries.length === 0)
return;
if (typeof enumObj[entries[0][0]] === "string") {
this._processSimpleEnum(entries);
} else {
this._processObjectEnum(entries, { labelKeys, valueKeys, key });
}
if (this.isEnumStore) {
enumStore.register(this.name, this);
}
}
/**
* 设置严格模式
* @param mode 是否启用严格模式
*/
setStrictMode(mode) {
this.strictMode = mode;
console.log(`Strict mode has been set to ${this.strictMode}`);
}
/**
* 设置默认语言
* @param lang 语言代码
*/
setDefaultLang(lang) {
this.defaultLang = lang;
}
/**
* 使用插件
* @param plugin 插件实例
*/
use(plugin) {
this.plugins.push(plugin);
}
/**
* @description 处理简单字符串枚举
* @private
* @param entries - 枚举项条目数组
*/
_processSimpleEnum(entries) {
for (const [k, text] of entries) {
const entry = { key: k, label: text };
this.entriesMapByKey.set(k, entry);
this.entriesMapByLabel.set(text, entry);
this.keys.push(k);
this.labels.push(text);
this.values.push(text);
}
}
/**
* @description 处理复杂对象枚举
* @private
* @param entries - 枚举项条目数组
* @param options - 处理选项
*/
_processObjectEnum(entries, options) {
for (const [_, item] of entries) {
const itemKey = EnumUtils$1.getValueFromConfig(item, options.key);
let itemText = EnumUtils$1.getValueFromConfig(item, options.labelKeys);
let itemValue = EnumUtils$1.getValueFromConfig(item, options.valueKeys);
const entry = { key: itemKey, label: itemText, value: itemValue };
this.entriesMapByKey.set(itemKey, entry);
this.entriesMapByLabel.set(itemText, entry);
this.labels.push(itemText);
this.keys.push(itemKey);
this.disposedList.push({ ...item, ...entry });
if (itemValue !== void 0) {
this.values.push(itemValue);
this.entriesMapByValue.set(itemValue, entry);
}
}
}
/**
* @description 根据键获取枚举项
* @param key - 要查找的键
* @returns 匹配的枚举项,如果不存在则返回 undefined
*/
getByKey(key) {
return this.entriesMapByKey.get(key);
}
/**
* @description 根据标签获取枚举项
* @param text - 要查找的标签文本
* @returns 匹配的枚举项,如果不存在则返回 undefined
*/
getByLabel(text) {
return this.entriesMapByLabel.get(text);
}
/**
* @description 根据值获取枚举项
* @param value - 要查找的值
* @returns 匹配的枚举项,如果不存在则返回 undefined
*/
getByValue(value) {
return this.entriesMapByValue.get(value);
}
/**
* @description 表格数据格式化函数
* @param r - 行数据(未使用)
* @param c - 列数据(未使用)
* @param val - 要格式化的值
* @returns 格式化后的标签文本
*/
tableFormatter(r, c, val) {
let entry;
if (typeof val === "string") {
entry = this.entriesMapByKey.get(val) || this.entriesMapByLabel.get(val);
} else {
entry = this.entriesMapByValue.get(val);
}
return entry ? entry.label : "notfound";
}
/**
* @description 获取所有枚举项的列表
*/
getAllItems() {
return Array.from(this.entriesMapByKey.values());
}
/**
* @description 检查是否存在某个键
*/
hasKey(key) {
return this.entriesMapByKey.has(key);
}
/**
* @description 检查是否存在某个值
*/
hasValue(value) {
return this.entriesMapByValue.has(value);
}
/**
* @description 检查是否存在某个标签
*/
hasLabel(text) {
return this.entriesMapByLabel.has(text);
}
/**
* @description 获取所有键的列表
*/
getKeys() {
return [...this.entriesMapByKey.keys()];
}
/**
* @description 获取所有标签的列表
*/
getLabels() {
return [...this.entriesMapByLabel.keys()];
}
/**
* @description 获取所有值的列表
*/
getValues() {
return [...this.entriesMapByValue.keys()];
}
/**
* @description 根据索引获取枚举项
*/
getByIndex(index) {
if (index >= 0 && index < this.keys.length) {
return this.getByKey(this.keys[index]);
}
return void 0;
}
/**
* 获取指定范围的枚举项
* @param range - 范围数组 [开始索引, 结束索引]
* @returns 指定范围的枚举项数组
*/
getRange(start, end) {
return this.keys.slice(start, end).map((key) => this.getByKey(key)).filter((item) => item);
}
/**
* 获取前n个数据项
*
* 根据提供的数量参数n,返回存储库中前n个数据项的列表。如果某些键没有对应的数据项,则这些键会被忽略。
*
* @param n - 需要获取的数据项的数量。
*
* @returns 返回包含前n个数据项的数组。如果n大于可用数据项的数量,则返回所有可用的数据项。
*/
getFirst(n) {
return this.keys.slice(0, n).map((key) => this.getByKey(key)).filter((item) => item);
}
/**
* @description 根据标签搜索数据项
*
* 在存储库中搜索具有特定标签的数据项。支持精确匹配和大小写敏感度选项。
*
* @param searchText - 用于搜索的文本。
* @param exactMatch - 是否需要精确匹配,默认为false,即允许部分匹配。
* @param caseSensitive - 是否区分大小写,默认为false,即不区分大小写。
*
* @returns 返回符合搜索条件的数据项列表。
*/
searchByLabel(searchText, exactMatch = false, caseSensitive = false) {
let searchFunc;
if (exactMatch) {
searchFunc = caseSensitive ? (text) => text === searchText : (text) => text.toLowerCase() === searchText.toLowerCase();
} else {
searchFunc = caseSensitive ? (text) => text.includes(searchText) : (text) => text.toLowerCase().includes(searchText.toLowerCase());
}
return Array.from(this.entriesMapByLabel.values()).filter((entry) => searchFunc(entry.label)).map((entry) => entry);
}
/**
* @description 根据值进行查找
* @param searchValue 要查找的值
* @param exactMatch 是否启用精确匹配,默认为false
* @param caseSensitive 是否区分大小写,默认为false
* @returns 返回匹配的枚举项列表
*/
searchByValue(searchValue, exactMatch = false, caseSensitive = false) {
let searchFunc;
if (auraShared.isString(searchValue)) {
if (exactMatch) {
searchFunc = caseSensitive ? (value) => value.toString() === searchValue : (value) => value.toString().toLowerCase() === searchValue.toLowerCase();
} else {
searchFunc = caseSensitive ? (value) => value.toString().includes(searchValue) : (value) => value.toString().toLowerCase().includes(searchValue.toLowerCase());
}
} else {
searchFunc = (value) => value === searchValue;
}
return Array.from(this.entriesMapByValue.values()).filter((entry) => entry.value !== void 0 && searchFunc(entry.value)).map((entry) => entry);
}
/**
* @description 带高亮的文本搜索
* @param searchText - 搜索文本
* @param options - 搜索选项
* @param options.caseSensitive - 是否区分大小写
* @param options.exactMatch - 是否精确匹配
* @param options.highlightColor - 高亮颜色
* @returns 带高亮标记的枚举项数组
*/
searchByTextWithHighlight(searchText, options = {}) {
const { caseSensitive = false, exactMatch = false, highlightColor = "#ffff00" } = options;
const searchFunc = exactMatch ? (text) => caseSensitive ? text === searchText : text.toLowerCase() === searchText.toLowerCase() : (text) => caseSensitive ? text.includes(searchText) : text.toLowerCase().includes(searchText.toLowerCase());
return Array.from(this.entriesMapByLabel.values()).filter((entry) => searchFunc(entry.label)).map((entry) => {
let highlightedText;
if (exactMatch) {
highlightedText = entry.label === searchText || !caseSensitive && entry.label.toLowerCase() === searchText.toLowerCase() ? `<mark style="background-color:${highlightColor};">${entry.label}</mark>` : entry.label;
} else {
const regExp = new RegExp(`(${searchText})`, caseSensitive ? "g" : "gi");
highlightedText = entry.label.replace(regExp, `<mark style="background-color:${highlightColor};">$1</mark>`);
}
return { ...entry, label: highlightedText };
});
}
/**
* @description 反转枚举项顺序
*/
reverse() {
this.keys.reverse();
this.labels.reverse();
this.values.reverse();
this.disposedList.reverse();
}
/**
* @description 基于自定义条件过滤枚举项
* @param predicate - 过滤函数
* @returns 过滤后的枚举项数组
*/
filter(predicate) {
return this.getAllItems().filter(predicate);
}
/**
* @description 根据条件过滤枚举项
* @param predicate - 过滤函数
* @returns 过滤后的枚举项数组
*/
updateItem(oldKey, newItem) {
const existingItem = this.getByKey(oldKey);
if (!existingItem) {
throw new Error(`Item with key ${oldKey} does not exist.`);
}
const updatedItem = { ...existingItem, ...newItem };
this.entriesMapByKey.set(updatedItem.key, updatedItem);
this.entriesMapByLabel.set(updatedItem.label, updatedItem);
if (updatedItem.value !== void 0) {
this.entriesMapByValue.set(updatedItem.value, updatedItem);
} else if (existingItem.value !== void 0) {
this.entriesMapByValue.delete(existingItem.value);
}
const index = this.keys.indexOf(oldKey);
this.keys[index] = updatedItem.key;
this.labels[index] = updatedItem.label;
this.values[index] = updatedItem.value;
}
/**
* 获取所有枚举条目
*/
getAllEntries() {
return Array.from(this.entriesMapByKey.values());
}
/**
* @description 删除枚举条目
* @param key 要删除的条目的键
*/
deleteEntry(key) {
const entry = this.entriesMapByKey.get(key);
if (!entry)
return;
this.entriesMapByKey.delete(key);
this.entriesMapByLabel.delete(entry.label);
if (entry.value !== void 0) {
this.entriesMapByValue.delete(entry.value);
}
this.keys = Array.from(this.entriesMapByKey.keys());
this.labels = Array.from(this.entriesMapByLabel.keys());
this.values = Array.from(this.entriesMapByValue.keys());
}
/**
* @description 更新枚举项
* @param key - 要更新的枚举项的键
* @param newItem - 新的枚举项数据
* @throws 如果指定的键不存在
*/
updateEntry(key, newItem) {
const originalEntry = this.entriesMapByKey.get(key);
if (!originalEntry)
return;
const updatedEntry = { ...originalEntry, ...newItem };
this.entriesMapByKey.set(key, updatedEntry);
this.entriesMapByLabel.delete(originalEntry.label);
this.entriesMapByLabel.set(updatedEntry.label, updatedEntry);
if (originalEntry.value !== void 0) {
this.entriesMapByValue.delete(originalEntry.value);
}
if (updatedEntry.value !== void 0) {
this.entriesMapByValue.set(updatedEntry.value, updatedEntry);
}
}
/**
* 将枚举项转换为下拉选择框选项格式
* @param valueField - 值字段名称,默认为 'value'
* @param labelField - 标签字段名称,默认为 'label'
* @param extraFields - 额外需要包含的字段
* @returns 选择框选项数组
*/
toSelectOptions(options = {}) {
const {
valueField = "value",
labelField = "label",
extraFields = []
} = options;
return this.disposedList.map((item) => {
var _a;
const result = {
[valueField]: (_a = item.value) != null ? _a : item.key,
[labelField]: item.label
};
extraFields.forEach((field) => {
if (field in item) {
result[field] = item[field];
}
});
return result;
});
}
/**
* 将枚举项转换为键值对对象
* @param valueType - 值的类型,'value' 使用 value 字段,'label' 使用 label 字段
* @returns 键值对对象
*/
toObject(valueType = "value") {
return this.getAllItems().reduce((acc, item) => {
var _a;
return {
...acc,
[item.key]: valueType === "value" ? (_a = item.value) != null ? _a : item.key : item.label
};
}, {});
}
/**
* 按照指定字段对枚举项进行排序
* @param field - 排序字段,可以是 'key', 'label', 'value' 或自定义函数
* @param direction - 排序方向,'asc' 或 'desc'
*/
sort(options) {
const { field, direction = "asc" } = options;
const sortFn = EnumUtils$1.createSortFunction(field);
this.disposedList.sort((a, b) => direction === "asc" ? sortFn(a, b) : sortFn(b, a));
this.keys = this.disposedList.map((item) => item.key);
this.labels = this.disposedList.map((item) => item.label);
this.values = this.disposedList.map((item) => item.value);
}
/**
* 批量添加枚举项
* @param items - 要添加的枚举项数组
* @throws 如果存在重复的键或值
*/
addBatch(items) {
const validations = items.map((item) => ({
item,
validation: this.validateItem(item)
}));
const invalidItems = validations.filter((v) => !v.validation.isValid);
if (invalidItems.length > 0) {
throw new Error(`Invalid items found: ${invalidItems.map((i) => `${i.item.key}: ${i.validation.errors.join(", ")}`).join("; ")}`);
}
items.forEach(this._addItem.bind(this));
}
/**
* 添加单个枚举项
*/
_addItem(item) {
this.entriesMapByKey.set(item.key, item);
this.entriesMapByLabel.set(item.label, item);
if (item.value !== void 0) {
this.entriesMapByValue.set(item.value, item);
}
this.disposedList.push(item);
this.keys.push(item.key);
this.labels.push(item.label);
if (item.value !== void 0) {
this.values.push(item.value);
}
}
/**
* 获取指定枚举项的相邻项
* @param key - 当前枚举项的键
* @returns 相邻项对象,包含前一项和后一项
*/
getAdjacent(key) {
const currentIndex = this.disposedList.findIndex((item) => item.key === key);
if (currentIndex === -1)
return {};
return {
prev: currentIndex > 0 ? this.disposedList[currentIndex - 1] : void 0,
next: currentIndex < this.disposedList.length - 1 ? this.disposedList[currentIndex + 1] : void 0
};
}
/**
* 导出为 JSON 字符串
* @param pretty - 是否美化输出
* @returns JSON 字符串
*/
toJSON(pretty = false) {
return JSON.stringify(this.disposedList, null, pretty ? 2 : 0);
}
/**
* 从 JSON 字符串导入枚举项
* @param json - JSON 字符串
* @throws 如果 JSON 格式无效
*/
static fromJSON(json) {
try {
const data = JSON.parse(json);
if (!auraShared.isArray(data)) {
throw new Error("JSON must be an array of enum items");
}
const enumObj = data.reduce((acc, item) => {
if (!item.key || !item.label) {
throw new Error("Invalid enum item format");
}
acc[item.key] = item;
return acc;
}, {});
return new EnumFactory(this.name, enumObj, { isEnumStore: false });
} catch (error) {
throw new Error(`Failed to parse JSON: ${error.message}`);
}
}
/**
* 创建枚举项的深拷贝
* @returns 新的枚举工厂实例
*/
clone() {
const clonedEnum = new EnumFactory(this.name, {});
clonedEnum.entriesMapByKey = new Map(this.entriesMapByKey);
clonedEnum.entriesMapByLabel = new Map(this.entriesMapByLabel);
clonedEnum.entriesMapByValue = new Map(this.entriesMapByValue);
clonedEnum.keys = [...this.keys];
clonedEnum.labels = [...this.labels];
clonedEnum.values = [...this.values];
clonedEnum.disposedList = JSON.parse(JSON.stringify(this.disposedList));
clonedEnum.originList = { ...this.originList };
return clonedEnum;
}
/**
* 验证枚举项是否有效
* @param item - 要验证的枚举项
* @returns 验证结果对象
*/
validateItem(item) {
const errors = [];
if (!item.key) {
errors.push("\u952E\u503C\u4E0D\u80FD\u4E3A\u7A7A");
}
if (!item.label) {
errors.push("\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A");
}
if (item.key && this.hasKey(item.key)) {
errors.push("\u952E\u503C\u5DF2\u5B58\u5728");
}
if (item.value !== void 0 && this.hasValue(item.value)) {
errors.push("\u503C\u5DF2\u5B58\u5728");
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* @description 更新内部数组
*/
_updateArraysFromDisposedList() {
this.keys.length = 0;
this.labels.length = 0;
this.values.length = 0;
this.disposedList.forEach((item) => {
this.keys.push(item.key);
this.labels.push(item.label);
if (item.value !== void 0) {
this.values.push(item.value);
}
});
}
/**
* @description 分页获取枚举项
* 该方法根据提供的分页选项对枚举项进行分页处理,并返回分页结果。
*
* @param options - 分页选项,包含以下属性:
* @param options.pageNum - 页码,默认为 1。
* @param options.pageSize - 每页的项数,默认为 10。
* @param options.withTotal - 是否包含总项数和总页数信息,默认为 true。
*
* @returns 分页结果对象,包含以下属性:
* @returns result.items - 分页后的枚举项数组。
* @returns result.pageNum - 当前页码。
* @returns result.pageSize - 每页的项数。
* @returns result.total - 总项数(如果 withTotal 为 true)。
* @returns result.totalPages - 总页数(如果 withTotal 为 true)。
*
* @example
* ```typescript
* // 示例使用
* const enumFactory = new EnumFactory({...});
* const paginationOptions: PaginationOptions = { pageNum: 2, pageSize: 5, withTotal: true };
* const paginationResult = enumFactory.paginate(paginationOptions);
* console.log(paginationResult);
* // 输出可能如下:
* // {
* // items: [枚举项数组],
* // pageNum: 2,
* // pageSize: 5,
* // total: 20,
* // totalPages: 4
* // }
* ```
*/
paginate(options) {
const { pageNum = 1, pageSize = 10, withTotal = true } = options;
const start = (pageNum - 1) * pageSize;
const end = start + pageSize;
const items = this.disposedList.slice(start, end);
const result = { items, pageNum, pageSize };
if (withTotal) {
result.total = this.disposedList.length;
result.totalPages = Math.ceil(this.disposedList.length / pageSize);
}
return result;
}
/**
* 将枚举项转换为 Map 对象
* 该方法根据指定的键字段和值字段,将枚举项列表转换为一个 Map 对象。
* 可以通过指定 `keyField` 和 `valueField` 来确定 Map 中的键和值的来源。
*
* @param keyField - 用于作为 Map 的键的枚举项字段,默认为 'key'。
* @param valueField - 用于作为 Map 的值的枚举项字段,默认为 'value'。
*
* @returns 转换后的 Map 对象,键为 `keyField` 对应的枚举项属性,值为 `valueField` 对应的枚举项属性。
*
* @example
* ```typescript
* // 示例使用
* const enumFactory = new EnumFactory({...});
* // 假设枚举项的结构为 { key: 'item1', label: 'Item One', value: 1 }
* const map = enumFactory.toMap();
* console.log(map);
* // 输出可能如下:
* // Map(1) { 'item1' => 1 }
*
* const mapWithCustomFields = enumFactory.toMap('label', 'key');
* console.log(mapWithCustomFields);
* // 输出可能如下:
* // Map(1) { 'Item One' => 'item1' }
* ```
*/
// public toMap(keyField: keyof EnumItem<T> = 'key', valueField: keyof EnumItem<T> = 'value'): Map<any, any> {
// return new Map(this.disposedList.map(item => [item[keyField], item[valueField]]));
// }
/**
* 批量更新枚举项
* @param updates - 更新数据,键为枚举项的key,值为要更新的数据
*/
batchUpdate(updates) {
Object.entries(updates).forEach(([key, updateData]) => {
const item = this.getByKey(key);
if (item)
this.updateItem(key, updateData);
});
}
/**
* 移动枚举项位置
* @param fromKey - 要移动的项的键
* @param toKey - 目标位置的项的键
*/
moveItem(fromKey, toKey) {
const fromIndex = this.disposedList.findIndex((item2) => item2.key === fromKey);
const toIndex = this.disposedList.findIndex((item2) => item2.key === toKey);
if (fromIndex === -1 || toIndex === -1)
return;
const [item] = this.disposedList.splice(fromIndex, 1);
this.disposedList.splice(toIndex, 0, item);
this._updateArraysFromDisposedList();
}
/**
* 对枚举项进行转换
* @param options - 转换选项
* @returns 转换后的数组
*/
transform(options = {}) {
const { includeUndefined = false, transform } = options;
return this.disposedList.filter((item) => includeUndefined || item.value !== void 0).map((item) => transform ? transform(item) : item);
}
/**
* 交换两个枚举项的位置
* @param key1 - 第一个项的键
* @param key2 - 第二个项的键
*/
swapItems(key1, key2) {
const index1 = this.disposedList.findIndex((item) => item.key === key1);
const index2 = this.disposedList.findIndex((item) => item.key === key2);
if (index1 === -1 || index2 === -1)
return;
[this.disposedList[index1], this.disposedList[index2]] = [this.disposedList[index2], this.disposedList[index1]];
this._updateArraysFromDisposedList();
}
/**
* 获取统计信息
* @returns 统计信息对象
*/
getStats() {
return {
total: this.disposedList.length,
withValue: this.disposedList.filter((item) => item.value !== void 0).length,
withoutValue: this.disposedList.filter((item) => item.value === void 0).length
};
}
/**
* 导出枚举数据
* 此方法根据提供的导出选项将枚举数据导出为不同的格式。支持的格式包括 JSON、CSV 和对象。
*
* @param options - 导出选项,包含以下属性:
* @param options.format - 导出格式,默认为 'json',可选值为 'json'、'csv'、'object' 等。
* @param options.pretty - 是否格式化输出,仅适用于 'json' 格式,默认为 false。
* @param options.fields - 要导出的字段列表,默认为 ['key', 'label', 'value']。
* @param options.separator - 仅适用于 CSV 格式,字段之间的分隔符,默认为 ','。
*
* @returns 导出的数据,格式为字符串。
*
* @throws Error 如果使用了不支持的导出格式,将抛出错误。
*
* @example
* ```typescript
* const enumFactory = new EnumFactory({...});
* const exportOptions: ExportOptions = { format: 'json', pretty: true };
* try {
* const exportedData = enumFactory.export(exportOptions);
* console.log(exportedData);
* } catch (error) {
* console.error(error.message);
* }
* ```
*/
export(options = {}) {
const {
format = "json",
pretty = false,
fields = ["key", "label", "value"],
separator = ","
} = options;
const data = this.disposedList.map(
(item) => fields.reduce((acc, field) => ({
...acc,
[field]: item[field]
}), {})
);
switch (format) {
case "json":
try {
return JSON.stringify(data, null, pretty ? 2 : 0);
} catch (jsonError) {
throw new Error(`Failed to export data in JSON format: ${jsonError.message}`);
}
case "csv":
try {
const header = fields.join(separator);
const rows = data.map((item) => fields.map((field) => {
var _a;
return String((_a = item[field]) != null ? _a : "");
}).join(separator));
return [header, ...rows].join("\n");
} catch (csvError) {
throw new Error(`Failed to export data in CSV format: ${csvError.message}`);
}
case "object":
try {
return JSON.stringify(data.reduce((acc, item) => ({ ...acc, [item.key]: item }), {}));
} catch (objectError) {
throw new Error(`Failed to export data in object format: ${objectError.message}`);
}
default:
throw new Error(`Unsupported format: ${format}`);
}
}
/**
* 比较两个枚举项
* @param item1 - 第一个枚举项
* @param item2 - 第二个枚举项
* @param options - 比较选项
* @returns 是否相等
*/
compareItems(item1, item2, options = {}) {
const { fields = ["key", "label", "value"], strict = true } = options;
return fields.every((field) => {
if (strict)
return item1[field] === item2[field];
return String(item1[field]) === String(item2[field]);
});
}
/**
* 执行枚举项的搜索操作。
* 该方法根据提供的查询字符串和搜索选项,在枚举项列表中查找匹配的项。
* 可以指定搜索的字段、是否区分大小写、是否进行模糊搜索和结果数量限制。
*
* @param query - 要搜索的字符串。
* @param options - 搜索选项,包含以下属性:
* @param options.fields - 搜索时要检查的字段列表,默认为 ['key', 'label']。
* @param options.caseSensitive - 是否区分大小写,默认为 false。
* @param options.fuzzy - 是否进行模糊搜索,默认为 true。
* @param options.limit - 结果数量的限制,默认为无限制。
*
* @returns 匹配的枚举项数组。
*
* @example
* ```typescript
* const enumFactory = new EnumFactory({...});
* // 假设枚举项列表包含:[{ key: 'item1', label: 'Item One', value: 1 }, { key: 'item2', label: 'Another Item', value: 2 }]
* // 执行搜索
* const searchOptions: SearchOptions = { fields: ['label'], caseSensitive: false, fuzzy: true, limit: 1 };
* const searchResults = enumFactory.search('item', searchOptions);
* console.log(searchResults);
* // 输出可能如下:
* // [{ key: 'item1', label: 'Item One', value: 1 }]
* ```
*/
search(query, options = {}) {
const {
fields = ["key", "label"],
caseSensitive = false,
fuzzy = true,
limit
} = options;
const searchQuery = caseSensitive ? query : query.toLowerCase();
const results = this.disposedList.filter((item) => {
return fields.some((field) => {
var _a;
const value = String((_a = item[field]) != null ? _a : "");
const compareValue = caseSensitive ? value : value.toLowerCase();
if (fuzzy)
return compareValue.includes(searchQuery);
return compareValue === searchQuery;
});
});
return limit ? results.slice(0, limit) : results;
}
addValidationRules(rules) {
this.validationRules.push(...rules);
}
/**
* 使用自定义规则验证枚举项
* @param item - 要验证的枚举项
* @returns 验证结果
*/
validateWithRules(item) {
const errors = [];
for (const rule of this.validationRules) {
const value = item[rule.field];
if (!rule.validator(value)) {
errors.push(rule.message);
}
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 获取重复项
* @param field - 要检查的字段
* @returns 重复的项
*/
getDuplicates(field) {
const valueMap = /* @__PURE__ */ new Map();
this.disposedList.forEach((item) => {
const value = item[field];
if (!valueMap.has(value)) {
valueMap.set(value, []);
}
valueMap.get(value).push(item);
});
return Array.from(valueMap.values()).filter((items) => items.length > 1).flat();
}
/**
* 合并另一个枚举工厂实例
* @param other - 要合并的枚举工厂实例
* @param strategy - 合并策略
*/
merge(other, strategy = "skip") {
other.disposedList.forEach((item) => {
const exists = this.hasKey(item.key);
if (exists) {
switch (strategy) {
case "skip":
return;
case "override":
this.updateItem(item.key, item);
break;
case "throw":
throw new Error(`Duplicate key found: ${item.key}`);
}
} else {
this._addItem(item);
}
});
}
/**
* 创建枚举项的快照
* @returns 快照数据
*/
createSnapshot() {
return JSON.stringify({
disposedList: this.disposedList,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
}
/**
* 从快照恢复
* @param snapshot - 快照数据
*/
restoreFromSnapshot(snapshot) {
try {
const { disposedList } = JSON.parse(snapshot);
this.entriesMapByKey.clear();
this.entriesMapByLabel.clear();
this.entriesMapByValue.clear();
this.disposedList.length = 0;
this.keys.length = 0;
this.labels.length = 0;
this.values.length = 0;
disposedList.forEach(this._addItem.bind(this));
} catch (error) {
throw new Error(`Failed to restore from snapshot: ${error.message}`);
}
}
/**
* 批量操作
* @param operations - 操作数组
*/
batch(operations) {
const snapshot = this.createSnapshot();
try {
operations.forEach((op) => {
switch (op.type) {
case "add":
if (op.data) {
this._addItem(op.data);
}
break;
case "update":
if (op.key && op.data) {
this.updateItem(op.key, op.data);
}
break;
case "delete":
if (op.key) {
this.deleteEntry(op.key);
}
break;
}
});
} catch (error) {
this.restoreFromSnapshot(snapshot);
throw error;
}
}
/**
* 高级分组功能
* @param options - 分组选项
* @returns 分组结果
*/
groupBy(options) {
const { by, sort = "asc", keepEmpty = false } = options;
const groups = {};
this.disposedList.forEach((item) => {
const key = typeof by === "function" ? by(item) : String(item[by]);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
});
Object.keys(groups).forEach((key) => {
if (!keepEmpty && groups[key].length === 0) {
delete groups[key];
return;
}
groups[key].sort((a, b) => {
const comparison = a.label.localeCompare(b.label);
return sort === "asc" ? comparison : -comparison;
});
});
return groups;
}
/**
* 带缓存的获取方法
* @param key - 缓存键
* @param fetcher - 数据获取函数
* @param options - 缓存选项
* @returns 缓存的结果
*/
getCached(key, fetcher, options = {}) {
const { ttl = 5e3, autoRefresh = false } = options;
const now = Date.now();
const cached = this.cache.get(key);
if (cached && now - cached.timestamp < ttl) {
return cached.value;
}
const value = fetcher();
this.cache.set(key, { value, timestamp: now });
if (autoRefresh) {
setTimeout(() => {
this.cache.delete(key);
}, ttl);
}
return value;
}
/**
* 比较两个枚举工厂实例的差异
* @param other - 要比较的枚举工厂实例
* @returns 差异结果
*/
diff(other) {
const result = {
added: [],
removed: [],
modified: []
};
other.disposedList.forEach((item) => {
const existing = this.getByKey(item.key);
if (!existing) {
result.added.push(item);
} else if (!this.compareItems(item, existing)) {
result.modified.push({
key: item.key,
before: existing,
after: item
});
}
});
this.disposedList.forEach((item) => {
if (!other.getByKey(item.key))
result.removed.push(item);
});
return result;
}
/**
* 获取详细统计信息
* @returns 统计信息
*/
getDetailedStats() {
const stats = {
total: this.disposedList.length,
byValue: {},
byLabel: {},
empty: 0,
distribution: {}
};
this.disposedList.forEach((item) => {
var _a;
const value = String((_a = item.value) != null ? _a : "undefined");
stats.byValue[value] = (stats.byValue[value] || 0) + 1;
stats.byLabel[item.label] = (stats.byLabel[item.label] || 0) + 1;
if (item.value === void 0) {
stats.empty++;
}
const valueType = typeof item.value;
stats.distribution[valueType] = (stats.distribution[valueType] || 0) + 1;
});
return stats;
}
/**
* 创建枚举项的迭代器
*/
*[Symbol.iterator]() {
yield* this.disposedList;
}
/**
* 批量验证
* @param items - 要验证的枚举项数组
* @returns 验证结果
*/
validateBatch(items) {
return items.map((item) => ({
item,
result: this.validateWithRules(item)
}));
}
/**
* 创建枚举项的深层代理
* @param key - 枚举项的键
* @returns 代理对象
*/
createProxy(key) {
const item = this.getByKey(key);
if (!item) {
throw new Error(`Item with key ${key} not found`);
}
return new Proxy(item, {
get: (target, prop) => {
if (prop === "value" && target.value === void 0) {
return target.key;
}
return target[prop];
},
set: (target, prop, value) => {
if (prop in target) {
this.updateItem(key, { [prop]: value });
return true;
}
return false;
}
});
}
// ... existing code ...
/**
* 添加观察者
* @param callback - 观察者回调函数
* @returns 取消订阅函数
*/
subscribe(callback) {
this.observers.add(callback);
return () => this.observers.delete(callback);
}
/**
* 通知所有观察者
*/
notifyObservers(action, item, metadata) {
this.observers.forEach((observer) => observer(action, item, metadata));
}
/**
* 记录历史
*/
addHistory(record) {
this.history.push({ ...record, timestamp: Date.now() });
}
/**
* 获取操作历史
* @param limit - 限制返回的记录数
*/
getHistory(limit) {
const records = [...this.history];
return limit ? records.slice(-limit) : records;
}
/**
* 设置元数据
*/
setMetadata(key, value) {
this.metadata.set(key, value);
}
/**
* 获取元数据
*/
getMetadata(key) {
return this.metadata.get(key);
}
/**
* 序列化枚举工厂
*/
serialize(options = {}) {
const { format = "json", compress = false, withMetadata = false } = options;
const data = {
items: this.disposedList,
...withMetadata ? { metadata: Object.fromEntries(this.metadata) } : {}
};
switch (format) {
case "json":
return JSON.stringify(data, null, compress ? 0 : 2);
case "yaml":
throw new Error("YAML format not implemented");
case "xml":
throw new Error("XML format not implemented");
default:
throw new Error(`Unsupported format: ${format}`);
}
}
/**
* 创建枚举项的时间序列
*/
createTimeSeries(timeField, valueField, interval = "day") {
return this.disposedList.sort((a, b) => new Date(a[timeField]).getTime() - new Date(b[timeField]).getTime()).map((item) => ({
time: new Date(item[timeField]).toISOString(),
value: item[valueField]
}));
}
/**
* 聚合计算
*/
aggregate(options) {
const { groupBy, aggregate, fn } = options;
const groups = this.groupBy({ by: groupBy });
const result = {};
for (const [key, items] of Object.entries(groups)) {
const values = items.map((item) => item[aggregate]);
if (typeof fn === "function") {
result[key] = fn(values);
} else {
switch (fn) {
case "sum":
result[key] = values.reduce((a, b) => Number(a) + Number(b), 0);
break;
case "avg":
result[key] = values.reduce((a, b) => Number(a) + Number(b), 0) / values.length;
break;
case "count":
result[key] = values.length;
break;
case "min":
result[key] = Math.min(...values.map(Number));
break;
case "max":
result[key] = Math.max(...values.map(Number));
break;
}
}
}
return result;
}
/**
* 批量更新带有乐观锁
*/
batchUpdateWithLock(updates) {
const result = { success: [], failed: [] };
updates.forEach(({ key, data, version }) => {
const item = this.getByKey(key);
if (!item || item.version !== version) {
result.failed.push(key);
return;
}
try {
this.updateItem(key, { ...data, version: version + 1 });
result.success.push(key);
} catch {
result.failed.push(key);
}
});
return result;
}
/**
* 创建枚举项的依赖图
*/
createDependencyGraph(dependencyField) {
const graph = /* @__PURE__ */ new Map();
this.disposedList.forEach((item) => {
const dependencies = item[dependencyField];
if (auraShared.isArray(dependencies)) {
graph.set(item.key, new Set(dependencies));
}
});
return graph;
}
/**
* 检查循环依赖
*/
checkCircularDependencies(dependencyField) {
const graph = this.createDependencyGraph(dependencyField);
const cycles = [];
const visited = /* @__PURE__ */ new Set();
const path = [];
function dfs(node) {
if (path.includes(node)) {
const cycle = path.slice(path.indexOf(node));
cycles.push(cycle);
return;
}
if (visited.has(node))
return;
visited.add(node);
path.push(node);
const dependencies = graph.get(node) || /* @__PURE__ */ new Set();
dependencies.forEach((dep) => dfs(dep));
path.pop();
}
graph.forEach((_, node) => dfs(node));
return cycles;
}
/**
* @description 创建枚举项的树形结构
*/
createTree(parentField, childrenField = "children") {
const items = [...this.disposedList];
const map = /* @__PURE__ */ new Map();
const roots = [];
items.forEach((item) => {
map.set(item.key, { ...item, [childrenField]: [] });
});
items.forEach((item) => {
const node = map.get(item.key);
const parentKey = item[parentField];
if (parentKey && map.has(parentKey)) {
map.get(parentKey)[childrenField].push(node);
} else {
roots.push(node);
}
});
return roots;
}
/**
* 执行自定义聚合操作
*/
customAggregate(aggregator, filter) {
const items = filter ? this.disposedList.filter(filter) : this.disposedList;
return aggregator(items);
}
/**
* @description 创建索引
*
* 根据提供的配置创建一个索引来加速数据查询。
* 如果设置了唯一约束(unique=true),则确保索引字段的组合值是唯一的。
*
* @param config - 索引配置对象,包含以下属性:
* - fields: (string[]) 必须,用于创建索引的数据项中的字段名数组。
* - unique: (boolean) 可选,默认为false。如果设置为true,则要求索引字段的组合值必须唯一。
* - name: (string) 可选,索引名称,默认使用fields数组连接后的字符串作为名称。
*
* @throws 当unique设置为true且发现重复值时,抛出错误提示违反唯一性约束。
*/
createIndex(config) {
const { fields, unique = false, name = fields.join("_") } = config;
const index = /* @__PURE__ */ new Map();
this.disposedList.forEach((item) => {
const indexKey = fields.map((field) => item[field]).join("|");
if (!index.has(indexKey)) {
index.set(indexKey, /* @__PURE__ */ new Set());
}
if (unique && index.get(indexKey).size > 0) {
throw new Error(`Unique constraint violation for index ${name}`);
}
index.get(indexKey).add(item.key);
});
this.indexes.set(name, index);
}
/**
* 高级查询
*
* 提供灵活的数据查询能力,包括过滤、排序、分页和字段选择。
*
* @param options - 查询选项对象,包含以下属性:
* - where: ({[key: string]: any}) 可选。用于过滤结果的条件。支持字符串匹配、正则表达式、函数等。
* - orderBy: ([string, 'asc' | 'desc'][]) 可选。指定排序规则的数组,每个元素是一个元组,第一个元素是字段名,第二个元素是排序方向。
* - offset: (number) 可选。指定从第几条记录开始返回。
* - limit: (number) 可选。限制返回的最大记录数。
* - select: (string[]) 可选。指定需要返回的字段名数组。
*
* @returns 返回符合查询条件的记录列表。
*
* @example
*
* * // 简单查询所有记录
* let result = repo.query({});
* console.log(result);
*
* // 查询名字为'John Doe'的记录
* result = repo.query({ where: { name: 'John Doe' } });
* console.log(result);
*
* // 查询年龄大于等于30的记录,并按年龄升序排序
* result = repo.query({
* where: { age: (age) => age >= 30 },
* orderBy: [['age', 'asc']]
* });
* console.log(result);
*
* // 分页获取数据,跳过第一条,限制返回两条
* result = repo.query({ offset: 1, limit: 2 });
* console.log(result);
*
* // 只选择特定字段
* result = repo.query({ select: ['name'] });
* console.log(result);
*
*
*/
query(options) {
let result = [...this.disposedList];
if (options.where) {
result = result.filter((item) => {
return Object.entries(options.where).every(([key, value]) => {
if (value instanceof RegExp) {
return value.test(String(item[key]));
}
if (typeof value === "function") {
return value(item[key]);
}
return item[key] === value;
});
});
}
if (options.orderBy) {
result.sort((a, b) => {
for (const [field, direction] of options.orderBy) {
const aVal = a[field];
const bVal = b[field];
const comparison = String(aVal).localeCompare(String(bVal));
if (comparison !== 0) {
return direction === "asc" ? comparison : -comparison;
}
}
return 0;
});
}
if (options.offset) {
result = result.slice(options.offset);
}
if (options.limit) {
result = result.slice(0, options.limit);
}
if (options.select) {
result = result.map((item) => {
const selected = {};
options.select.forEach((field) => {
selected[field] = item[field];
});
return selected;
});
}
return result;
}
/**
* 添加数据迁移
*
* 将新的迁移配置添加到现有的迁移列表中,并根据版本号对所有迁移进行排序。
* 这有助于确保在执行数据迁移时按照正确的顺序处理。
*
* @param migration - 迁移配置对象,包含以下属性:
* - version: (number) 必须。表示迁移的版本号,用于确定迁移执行的顺序。
* - migrate: ((data: T[]) => void) 必须。实际执行迁移操作的函数,接收当前的数据列表作为参数。
*/
addMigration(migration) {
this.migrations.push(migration);
this.migrations.sort((a, b) => a.version - b.version);
}
/**
* 执行数据迁移
* @param targetVersion - 目标版本
*/
async migrate(targetVersion) {
const currentVersion = this.version;
if (targetVersion > currentVersion) {
for (const migration of this.migrations) {
if (migration.version <= currentVersion)
continue;
if (migration.version > targetVersion)
break;
this.disposedList = await migration.up(this.disposedList);
}
} else if (targetVersion < currentVersion) {
for (const migration of this.migrations.reverse()) {
if (migration.version > currentVersion)
continue;
if (migration.version <= targetVersion)
break;
this.disposedList = await migration.down(this.disposedList);
}
}
this.version = targetVersion;
this._updateArraysFromDisposedList();
}
/**
* 从缓存恢复
* @param key - 缓存键
* @param strategy - 缓存策略
*/
restoreFromCache(key, strategy = "memory") {
var _a;
let data = null;
switch (strategy) {
case "memory":
const cached = this.cache.get(key);
data = (_a = cached == null ? void 0 : cached.value) != null ? _a : null;
break;
case "localStorage":
data = localStorage.getItem(key);
break;
case "sessionStorage":
data = sessionStorage.getItem(key);
break;
}
if (data) {
const parsed = JSON.parse(data);
this.disposedList = parsed.items;
if (parsed.metadata) {
this.metadata = new Map(Object.entries(parsed.metadata));
}
this._updateArraysFromDisposedList();
return true;
}
return false;
}
/**
* 批量导入数据
* @param items - 要导入的数据
* @param options - 导入选项
*/
async bulkImport(items, options = {}) {
const { skipValidation = false, batchSize = 100, onProgress } = options;
const result = { success: 0, failed: 0, errors: [] };
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
try {
if (!skipValidation) {
const validations = this.validateBatch(batch);
const invalid = validations.filter((v) => !v.result.isValid);
if (invalid.length > 0) {
throw new Error(`Validation failed for ${invalid.length} items`);
}
}
await Promise.all(batch.map(async (item) => {
try {
this._addItem(item);
result.success++;
} catch (error) {
result.failed++;
result.errors.push(error);
}
}));
if (onProgress)
onProgress((i + batch.length) / items.length);
} catch (error) {
result.failed += batch.length;
result.errors.push(error);
}
}
return result;
}
/**
* 创建枚举项的快照并返回差异
* @param snapshot - 快照数据
*/
diffWithSnapshot(snapshot) {
const snapshotData = JSON.parse(snapshot);
const currentKeys = new Set(this.keys);
const snapshotKeys = new Set(snapshotData.items.map((item) => item.key));
const added = this.disposedList.filter((item) => !snapshotKeys.has(item.key));
const removed = snapshotData.items.filter((item) => !currentKeys.has(item.key));
const modified = this.disposedList.filter((current) => {
const snapshot2 = snapshotData.items.find((item) => item.key === current.key);
return snapshot2 && !this.compareItems(current, snapshot2);
}).map((item) => ({
key: item.key,
changes: this.getItemChanges(
item,
snapshotData.items.find((i) => i.key === item.key)
)
}));
return { added, removed, modified };
}
/**
* 获取两个枚举项之间的差异
*/
getItemChanges(current, previous) {
const changes = {};
Object.keys(current).forEach((key) => {
if (current[key] !== previous[key]) {
changes[key] = current[key];
}
});
return changes;
}
/**
* 将枚举列表转换为树形结构
* @param options - 树形结构配置选项
* @returns 树形结构数组
* @example
*
* ```
* // 基本树形结构转换
* const tree = enumFactory.toTree({
* parentField: 'parentId',
* childrenField: 'children',
* rootParentValue: null
* });
*
* // 带有自定义转换的树形结构
* const customTree = enumFactory.toTree({
* parentField: 'parentId',
* fieldMapping: {
* key: 'id',
* label: 'name'
* },
* transform: (node) => ({
* ...node,
* title: `${node.label} (${node.key})`,
* expanded: false
* }),
* sort: (a, b) => a.label.localeCompare(b.label),
* withTreeInfo: true
* });
*/
toTree(options) {
const {
parentField,
childrenField = "children",
rootParentValue = null,
fieldMapping = {},
transform,
sort,
keepEmptyChildren = false,
withTreeInfo = false
} = options;
const nodeMap = /* @__PURE__ */ new Map();
const roots = [];
this.disposedList.forEach((item) => {
const node = transform ? transform(item) : Object.entries(item).reduce((acc, [key, value]) => ({
...acc,
[fieldMapping[key] || key]: value
}), {});
node[childrenField] = [];
nodeMap.set(item.key, node);
});
this.disposedList.forEach((item) => {
const node = nodeMap.get(item.key);
const parentKey = item[parentField];
if (parentKey === rootParentValue) {
roots.push(node);
} else {
const parentNode = nodeMap.get(parentKey);
if (parentNode) {
parentNode[childrenField].push(node);
} else {
roots.push(node);
}
}
});
const processNode = (node, level = 0, parentPath = []) => {
const currentPath = [...parentPath, node.key];
const children = node[childrenField];
if (withTreeInfo) {
node.treeInfo = {
level,
path: currentPath,
isLeaf: children.length === 0,
parentKey: parentPath[parentPath.length - 1],
index: children.length,
siblings: children.length
};
}
if (sort && children.length > 0) {
children.sort(sort);
}
children.forEach((child, index) => {
if (withTreeInfo) {
child.treeInfo = {
...child.treeInfo,
index,
siblings: children.length
};
}
processNode(child, level + 1, currentPath);
});
if (!keepEmptyChildren && children.length === 0) {
delete node[childrenField];
}
};
if (sort) {
roots.sort(sort);
}
roots.forEach((root, index) => {
if (withTreeInfo) {
root.treeInfo = {
...root.treeInfo,
index,
siblings: roots.length
};
}
processNode(root);
});
return roots;
}
/**
* 在树形结构中查找节点
* @param predicate - 查找条件函数,接收节点和上下文参数
* @param options - 查找选项
* @param options.mode - 搜索模式,'dfs'(深度优先) 或 'bfs'(广度优先),默认 'dfs'
* @param options.findAll - 是否查找所有匹配项,默认 false
* @param options.maxDepth - 最大搜索深度,默认 Infinity
* @param options.includeAncestors - 是否包含祖先节点,默认 false
* @param options.includeDescendants - 是否包含后代节点,默认 false
* @param options.resultTransform - 结果转换函数
* @returns 查找结果数组
*
* @example
* // 基本查找
* const result = enumFactory.findInTree(
* node => node.key === 'targetKey',
* { parentField: 'parentId' }
* );
*
* // 高级查找示例
* const results = enumFactory.findInTree(
* (node, context) => {
* return node.value > 100 &&
* context.depth < 3 &&
* context.ancestors.some(a => a.type === 'folder');
* },
* {
* parentField: 'parentId',
* mode: 'bfs',
* findAll: true,
* includeAncestors: true,
* maxDepth: 3
* }
* );
*
* // 使用结果转换
* const transformedResults = enumFactory.findInTree(
* node => node.type === 'file',
* {
* parentField: 'parentId',
* resultTransform: (node, context) => ({
* id: node.key,
* path: context.path.map(n => n.label).join('/')
* })
* }
* );
*
* @test
* it('should find node by key', () => {
* const result = enumFactory.findInTree(node => node.key === 'key1');
* expect(result[0].node.key).toBe('key1');
* });
*
* it('should respect maxDepth option', () => {
* const results = enumFactory.findInTree(
* () => true,
* { maxDepth: 2, findAll: true }
* );
* expect(results.every(r => r.depth <= 2)).toBe(true);
* });
*/
findInTree(predicate, options = {
parentField: ""
}) {
const {
mode = "dfs",
findAll = false,
maxDepth = Infinity,
includeAncestors = false,
includeDescendants = false,
resultTransform,
...treeOptions
} = options;
const tree = this.toTree(treeOptions);
const results = [];
let matchCount = 0;
const getDescendants = (node) => {
const descendants = [];
const stack = [...node.children || []];
whil