oak-frontend-base/es/features/runningTree.js

1848 lines
63 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { assert } from 'oak-domain/lib/utils/assert';
import { cloneDeep, unset, uniq } from "oak-domain/lib/utils/lodash";
import { checkFilterContains, combineFilters } from "oak-domain/lib/store/filter";
import { createOperationsFromModies } from 'oak-domain/lib/store/modi';
import { judgeRelation } from "oak-domain/lib/store/relation";
import { Feature } from '../types/Feature';
import { generateNewId } from 'oak-domain/lib/utils/uuid';
export const MODI_NEXT_PATH_SUFFIX = ':next';
class Node extends Feature {
entity;
// protected fullPath: string;
schema;
projection; // 只在Page层有
parent;
dirty;
cache;
loading;
loadingMore;
executing;
modiIds; // 对象所关联的modi
actions;
cascadeActions;
relationAuth;
constructor(entity, schema, cache, relationAuth, projection, parent, path, actions, cascadeActions) {
super();
this.entity = entity;
this.schema = schema;
this.cache = cache;
this.relationAuth = relationAuth;
this.projection = projection;
this.parent = parent;
this.dirty = undefined;
this.loading = 0;
this.loadingMore = false;
this.executing = false;
this.modiIds = undefined;
this.actions = actions;
this.cascadeActions = cascadeActions;
}
getEntity() {
return this.entity;
}
getSchema() {
return this.schema;
}
/**
* 这个函数从某个结点向父亲查询看所在路径上是否有需要被应用的modi
*/
getActiveModiOperations() {
const { modiIds } = this;
if (modiIds && modiIds.length > 0) {
const modies = this.cache.get('modi', {
data: {
id: 1,
targetEntity: 1,
entity: 1,
entityId: 1,
iState: 1,
action: 1,
data: 1,
filter: 1,
},
filter: {
id: {
$in: modiIds,
},
iState: 'active',
}
});
assert(modies);
return createOperationsFromModies(modies);
}
// 如果当前层没有向上查找。只要有就返回目前应该不存在多层modi
if (this.parent) {
if (this.parent instanceof ListNode || this.parent instanceof SingleNode) {
return this.parent.getActiveModiOperations();
}
}
}
setDirty() {
if (!this.dirty) {
this.dirty = true;
}
// 现在必须要将父结点都置dirty了再publish因为整棵树是在一起redoOperation了
if (this.parent) {
this.parent.setDirty();
}
this.publish();
}
isDirty() {
return !!this.dirty;
}
isLoading() {
return !!this.loading;
}
setLoading(loading) {
this.loading = loading;
}
isLoadingMore() {
return this.loadingMore;
}
isExecuting() {
return this.executing;
}
setExecuting(executing) {
this.executing = executing;
this.publish();
}
getParent() {
return this.parent;
}
getProjection() {
const projection = typeof this.projection === 'function' ? this.projection() : (this.projection && cloneDeep(this.projection));
return projection;
}
setProjection(projection) {
assert(!this.projection);
this.projection = projection;
}
judgeRelation(attr) {
const attr2 = attr.split(':')[0]; // 处理attr:prev
return judgeRelation(this.schema, this.entity, attr2);
}
}
const DEFAULT_PAGINATION = {
currentPage: 0,
pageSize: 20,
more: true,
total: 0,
};
class ListNode extends Node {
updates;
children = {};
filters;
sorters;
getTotal;
pagination = DEFAULT_PAGINATION;
sr = {};
syncHandler;
setFiltersAndSortedApplied() {
this.filters.forEach(ele => ele.applied = true);
this.sorters.forEach(ele => ele.applied = true);
for (const k in this.children) {
this.children[k].setFiltersAndSortedApplied();
}
}
setLoading(loading) {
if (this.loading === 0) {
super.setLoading(loading);
for (const k in this.children) {
this.children[k].setLoading(loading);
}
}
}
setUnloading(loading) {
if (this.loading === loading) {
super.setLoading(0);
for (const k in this.children) {
this.children[k].setUnloading(loading);
}
}
}
startLoading() {
const number = Math.random() + 1;
this.setLoading(number);
}
endLoading() {
const number = this.loading;
this.setUnloading(number);
}
checkIfClean() {
if (Object.keys(this.updates).length > 0) {
return;
}
for (const k in this.children) {
if (this.children[k].isDirty()) {
return;
}
}
if (this.isDirty()) {
this.dirty = false;
this.parent?.checkIfClean();
}
}
onCacheSync(records) {
// 只需要处理当listNode为首页且插入/删除项满足条件的情况
if (this.loading || this.pagination.currentPage !== 0) {
return;
}
for (const record of records) {
const { a } = record;
switch (a) {
case 'c': {
const { e, d } = record;
if (e === this.entity) {
const context = this.cache.begin();
const { filter } = this.constructSelection();
if (d instanceof Array) {
d.forEach((dd) => {
if (!filter || (!this.sr.hasOwnProperty(dd.id) && checkFilterContains(e, context, filter, {
id: dd.id,
}, true))) {
this.sr[dd.id] = {}; // 如果有aggr怎么办一般有aggr的页面不会出现这种情况以后再说
if (typeof this.pagination.total === 'number') {
this.pagination.total += 1;
}
}
});
}
else {
if (!filter || (!this.sr.hasOwnProperty(d.id) && checkFilterContains(e, context, filter, {
id: d.id,
}, true))) {
this.sr[d.id] = {}; // 如果有aggr怎么办一般有aggr的页面不会出现这种情况以后再说
if (typeof this.pagination.total === 'number') {
this.pagination.total += 1;
}
}
}
this.cache.commit();
}
break;
}
case 'r': {
const { e, f } = record;
if (e === this.entity) {
if (!f) {
this.sr = {};
this.pagination.total = 0;
}
else if (f.id && typeof f.id === 'string') {
// 绝大多数删除情况
if (this.sr.hasOwnProperty(f.id)) {
unset(this.sr, f.id);
if (typeof this.pagination.total === 'number') {
this.pagination.total -= 1;
}
}
}
else {
const context = this.cache.begin();
for (const id in this.sr) {
if (!f || checkFilterContains(e, context, f, { id }, true)) {
unset(this.sr, id);
if (typeof this.pagination.total === 'number') {
this.pagination.total -= 1;
}
}
}
this.cache.commit();
}
}
break;
}
default: {
break;
}
}
}
}
destroy() {
this.cache.unbindOnSync(this.syncHandler);
for (const k in this.children) {
this.children[k].destroy();
}
}
constructor(entity, schema, cache, relationAuth, projection, parent, path, filters, sorters, getTotal, pagination, actions, cascadeActions) {
super(entity, schema, cache, relationAuth, projection, parent, path, actions, cascadeActions);
this.filters = filters || [];
this.sorters = sorters || [];
this.getTotal = getTotal;
this.sr = {};
this.pagination = pagination ? {
...pagination,
currentPage: pagination.currentPage - 1,
more: true,
total: 0,
} : DEFAULT_PAGINATION;
this.updates = {};
this.syncHandler = (records) => this.onCacheSync(records);
this.cache.bindOnSync(this.syncHandler);
if (parent) {
assert(path);
parent.addChild(path, this);
}
}
getPagination() {
return this.pagination;
}
setPagination(pagination, dontRefresh) {
const newPagination = Object.assign(this.pagination, pagination);
this.pagination = newPagination;
if (!dontRefresh) {
this.refresh();
}
}
addChild(path, node) {
assert(!this.children[path]);
// assert(path.length > 10, 'List的path改成了id');
this.children[path] = node;
assert(this.sr); // listNode的子结点不可能在取得数据前就建立吧 by Xc
node.saveRefreshResult({
[path]: this.sr[path] || {},
});
}
removeChild(path) {
unset(this.children, path);
}
getChild(path) {
return this.children[path];
}
getNamedFilters() {
return this.filters;
}
getNamedFilterByName(name) {
const filter = this.filters.find((ele) => ele['#name'] === name);
return filter;
}
setNamedFilters(filters, refresh) {
this.filters = filters.map((ele) => Object.assign({}, ele, { applied: false }));
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
addNamedFilter(filter, refresh) {
// filter 根据#name查找
const fIndex = this.filters.findIndex((ele) => filter['#name'] && ele['#name'] === filter['#name']);
if (fIndex >= 0) {
this.filters.splice(fIndex, 1, Object.assign({}, filter, { applied: false }));
}
else {
this.filters.push(Object.assign({}, filter, { applied: false }));
}
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
removeNamedFilter(filter, refresh) {
// filter 根据#name查找
const fIndex = this.filters.findIndex((ele) => filter['#name'] && ele['#name'] === filter['#name']);
if (fIndex >= 0) {
this.filters.splice(fIndex, 1);
}
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
removeNamedFilterByName(name, refresh) {
// filter 根据#name查找
const fIndex = this.filters.findIndex((ele) => ele['#name'] === name);
if (fIndex >= 0) {
this.filters.splice(fIndex, 1);
}
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
getNamedSorters() {
return this.sorters;
}
getNamedSorterByName(name) {
const sorter = this.sorters.find((ele) => ele && ele['#name'] === name);
return sorter;
}
setNamedSorters(sorters, refresh) {
this.sorters = sorters.map(ele => Object.assign({}, ele, { applied: false }));
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
addNamedSorter(sorter, refresh) {
// sorter 根据#name查找
const fIndex = this.sorters.findIndex((ele) => sorter['#name'] && ele['#name'] === sorter['#name']);
if (fIndex >= 0) {
this.sorters.splice(fIndex, 1, Object.assign({}, sorter, { applied: false }));
}
else {
this.sorters.push(Object.assign({}, sorter, { applied: false }));
}
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
removeNamedSorter(sorter, refresh) {
// sorter 根据#name查找
const fIndex = this.sorters.findIndex((ele) => sorter['#name'] && ele['#name'] === sorter['#name']);
if (fIndex >= 0) {
this.sorters.splice(fIndex, 1);
}
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
removeNamedSorterByName(name, refresh) {
// sorter 根据#name查找
const fIndex = this.sorters.findIndex((ele) => ele['#name'] === name);
if (fIndex >= 0) {
this.sorters.splice(fIndex, 1);
}
if (refresh) {
this.refresh(0, false);
}
else {
this.publish();
}
}
getFreshValue() {
/**
* 现在简化情况只取sr中有id的数据
*/
const ids = Object.keys(this.sr);
const { data, sorter } = this.constructSelection(true, false, true);
const result = this.cache.get(this.entity, {
data,
filter: {
id: {
$in: ids,
}
},
sorter,
}, true, this.sr);
return result;
}
addItem(item) {
// 如果数据键值是一个空字符串则更新成null
for (const k in item) {
if (item[k] === '') {
Object.assign(item, {
[k]: null,
});
}
}
const id = generateNewId();
assert(!this.updates[id]);
this.updates[id] = {
id: generateNewId(),
action: 'create',
data: Object.assign(item, { id }),
};
this.sr[id] = {};
this.setDirty();
return id;
}
removeItem(id) {
if (this.updates[id] &&
this.updates[id].action === 'create') {
// 如果是新增项,在这里抵消
unset(this.updates, id);
unset(this.sr, id);
}
else {
this.updates[id] = {
id: generateNewId(),
action: 'remove',
data: {},
filter: {
id,
},
};
}
this.setDirty();
}
recoverItem(id) {
const operation = this.updates[id];
assert(operation?.action === 'remove');
unset(this.updates, id);
this.setDirty();
}
resetItem(id) {
const operation = this.updates[id];
assert(operation?.action === 'update');
unset(this.updates, id);
this.setDirty();
}
/**
* 目前只支持根据itemId进行更新
* @param data
* @param id
* @param beforeExecute
* @param afterExecute
*/
updateItem(data, id, action) {
// 如果数据键值是一个空字符串则更新成null
for (const k in data) {
if (data[k] === '') {
Object.assign(data, {
[k]: null,
});
}
}
if (this.children && this.children[id]) {
// 实际中有这样的case出现当使用actionButton时。先这样处理。by Xc 20230214
return this.children[id].update(data, action);
}
if (this.updates[id]) {
const operation = this.updates[id];
const { data: dataOrigin } = operation;
Object.assign(dataOrigin, data);
if (action && operation.action !== action) {
assert(operation.action === 'update');
operation.action = action;
}
}
else {
this.updates[id] = {
id: generateNewId(),
action: action || 'update',
data,
filter: {
id,
},
};
}
this.setDirty();
}
updateItems(data, action) {
for (const id in data) {
this.updateItem(data[id], id, action);
}
}
composeOperations() {
if (!this.dirty) {
return;
}
const operations = [];
for (const id in this.updates) {
if (this.updates[id]) {
operations.push({
entity: this.entity,
operation: cloneDeep(this.updates[id]),
});
}
}
for (const id in this.children) {
const childOperation = this.children[id].composeOperations();
if (childOperation) {
// 现在因为后台有not null检查不能先create再update所以还是得合并成一个
assert(childOperation.length === 1);
const { operation } = childOperation[0];
const { action, data, filter } = operation;
assert(!['create', 'remove'].includes(action), '在list结点上对子结点进行增删请使用父结点的addItem/removeItem不要使用子结点的create/remove');
assert(filter.id === id);
const sameNodeOperation = operations.find(ele => ele.operation.action === 'create' && ele.operation.data.id === id || ele.operation.filter?.id === id);
if (sameNodeOperation) {
if (sameNodeOperation.operation.action !== 'remove') {
Object.assign(sameNodeOperation.operation.data, data);
}
}
else {
operations.push(...childOperation);
}
}
}
return operations;
}
getProjection() {
const projection = super.getProjection();
// List必须自主决定Projection
/* if (this.children.length > 0) {
const subProjection = await this.children[0].getProjection();
return merge(projection, subProjection);
} */
return projection;
}
constructFilters(withParent, ignoreNewParent, ignoreUnapplied) {
const { filters: ownFilters } = this;
const filters = ownFilters.filter(ele => !ignoreUnapplied || ele.applied === true || ele.applied === undefined // 如果是undefined说明不可以移除构造时就存在也得返回
).map((ele) => {
const { filter } = ele;
if (typeof filter === 'function') {
return filter();
}
return filter;
});
if (withParent && this.parent) {
if (this.parent instanceof SingleNode) {
const filterOfParent = this.parent.getParentFilter(this, ignoreNewParent);
if (filterOfParent) {
filters.push(filterOfParent);
}
else {
// 说明有父结点但是却没有相应的约束此时不应该去refresh(是一个insert动作)
return undefined;
}
}
}
// 返回的filter在上层做check的时候可能被改造掉
return cloneDeep(filters);
}
constructSelection(withParent, ignoreNewParent, ignoreUnapplied) {
const { sorters, getTotal } = this;
const data = this.getProjection();
assert(data, '取数据时找不到projection信息');
const sorterArr = sorters.filter(ele => !ignoreUnapplied || ele.applied).map((ele) => {
const { sorter } = ele;
if (typeof sorter === 'function') {
return sorter();
}
return sorter;
}).flat().filter((ele) => !!ele);
const filters = this.constructFilters(withParent, ignoreNewParent, ignoreUnapplied);
const filters2 = filters?.filter((ele) => !!ele);
const filter = filters2 ? combineFilters(this.entity, this.schema, filters2) : undefined;
const { currentPage, pageSize } = this.pagination;
return {
data,
filter,
sorter: sorterArr,
total: getTotal,
indexFrom: currentPage * pageSize,
count: pageSize,
};
}
/**
* 存留查询结果
*/
saveRefreshResult(sr, append, currentPage) {
const { data, total } = sr;
if (data) {
this.pagination.more = Object.keys(data).length === this.pagination.pageSize;
}
if (currentPage) {
this.pagination.currentPage = currentPage;
}
if (typeof total === 'number') {
this.pagination.total = total;
}
if (append) {
this.sr = {
...this.sr,
...data,
};
}
else {
this.sr = data || {};
}
for (const k in this.children) {
const child = this.children[k];
child.saveRefreshResult({
[k]: this.sr[k] || {},
});
}
this.publish();
}
async refresh(pageNumber, append) {
const { entity, pagination } = this;
const { currentPage, pageSize, randomRange } = pagination;
const currentPage3 = typeof pageNumber === 'number' ? pageNumber : currentPage;
assert(!randomRange || !currentPage3, 'list在访问数据时如果设置了randomRange则不应再有pageNumber');
const { data: projection, filter, sorter, total, } = this.constructSelection(true, true);
// 若不存在有效的过滤条件若有父结点但却为空时说明父结点是一个create动作不用刷新则不能刷新
if ((!this.getParent() || this.getParent() instanceof VirtualNode || filter) && projection) {
try {
this.startLoading();
if (append) {
this.loadingMore = true;
}
this.publish();
await this.cache.refresh(entity, {
data: projection,
filter,
sorter,
indexFrom: currentPage3 * pageSize,
count: pageSize,
randomRange,
total: currentPage3 === 0 ? total : undefined,
}, undefined, (selectResult) => {
this.endLoading();
this.setFiltersAndSortedApplied();
if (append) {
this.loadingMore = false;
}
this.saveRefreshResult(selectResult, append, currentPage3);
});
}
catch (err) {
this.endLoading();
if (append) {
this.loadingMore = false;
}
this.publish();
throw err;
}
}
else {
// 不刷新也publish一下触发页面reRender不然有可能导致页面不进入formData
this.publish();
}
}
async loadMore() {
const { filters, sorters, pagination, entity } = this;
const { pageSize, more, currentPage } = pagination;
if (!more) {
return;
}
const currentPage2 = currentPage + 1;
await this.refresh(currentPage2, true);
}
setCurrentPage(currentPage, append) {
this.refresh(currentPage, append);
}
clean() {
if (this.dirty) {
const originUpdates = this.updates;
this.updates = {};
for (const k in this.children) {
this.children[k].clean();
}
for (const k in originUpdates) {
if (originUpdates[k].action === 'create') {
unset(this.sr, originUpdates[k].data.id);
}
}
this.dirty = undefined;
this.publish();
}
}
// 查看这个list上所有数据必须遵守的限制
getIntrinsticFilters() {
const filters = this.constructFilters(undefined, true, true);
return combineFilters(this.entity, this.schema, filters || []);
}
}
class SingleNode extends Node {
id;
sr;
children;
filters;
operation;
constructor(entity, schema, cache, relationAuth, projection, parent, path, id, filters, actions, cascadeActions) {
super(entity, schema, cache, relationAuth, projection, parent, path, actions, cascadeActions);
this.children = {};
this.sr = {};
this.filters = filters;
// addChild有可能为本结点赋上id值所以要先行
if (parent) {
assert(path);
parent.addChild(path, this);
}
if (!this.id && !id) {
this.create({});
}
else if (id) {
if (this.id) {
assert(id === this.id, 'singleNode初始化的id必须一致');
}
else {
this.id = id;
}
}
}
setFiltersAndSortedApplied() {
for (const k in this.children) {
this.children[k].setFiltersAndSortedApplied();
}
}
setLoading(loading) {
if (this.loading === 0) {
super.setLoading(loading);
for (const k in this.children) {
this.children[k].setLoading(loading);
}
}
}
setUnloading(loading) {
if (this.loading === loading) {
super.setLoading(0);
for (const k in this.children) {
this.children[k].setUnloading(loading);
}
}
}
startLoading() {
const number = Math.random() + 1;
this.setLoading(number);
}
endLoading() {
const loading = this.loading;
this.setUnloading(loading);
}
checkIfClean() {
if (this.operation) {
return;
}
for (const k in this.children) {
if (this.children[k].isDirty()) {
return;
}
}
if (this.isDirty()) {
this.dirty = false;
this.parent?.checkIfClean();
}
}
destroy() {
for (const k in this.children) {
this.children[k].destroy();
}
}
getChild(path) {
return this.children[path];
}
setId(id) {
if (id !== this.id) {
// 如果本身是create 这里无视就行(因为框架原因会调用一次)
if (this.operation?.action === 'create') {
if (this.operation.data.id === id) {
return;
}
}
assert(!this.dirty, 'setId时结点是dirty在setId之前应当处理掉原有的update');
this.id = id;
this.publish();
}
}
unsetId() {
if (this.id) {
this.id = undefined;
this.publish();
}
}
getId() {
if (this.id) {
return this.id;
}
if (this.operation && this.operation?.action === 'create') {
return this.operation.data.id;
}
}
getChildren() {
return this.children;
}
addChild(path, node) {
assert(!this.children[path]);
this.children[path] = node;
this.passRsToChild(path);
}
removeChild(path) {
unset(this.children, path);
}
getFreshValue() {
const projection = this.getProjection(false);
const id = this.getId();
if (projection && id) {
const result = this.cache.get(this.entity, {
data: projection,
filter: {
id,
},
}, true, {
[id]: this.sr,
});
return result[0];
}
}
create(data) {
const id = generateNewId();
assert(!this.id && !this.dirty, 'create前要保证singleNode为空');
// 如果数据键值是一个空字符串则更新成null
for (const k in data) {
if (data[k] === '') {
Object.assign(data, {
[k]: null,
});
}
}
this.operation = {
id: generateNewId(),
action: 'create',
data: Object.assign({}, data, { id }),
};
this.setDirty();
}
update(data, action) {
// 如果数据键值是一个空字符串则更新成null
for (const k in data) {
if (data[k] === '') {
Object.assign(data, {
[k]: null,
});
}
}
if (!this.operation) {
if (this.id) {
const operation = {
id: generateNewId(),
action: action || 'update',
data,
};
Object.assign(operation, {
filter: {
id: this.id,
},
});
this.operation = operation;
}
else {
// 有可能是create上层不考虑两者的区别
assert(!action);
const operation = {
id: generateNewId(),
action: 'create',
data: Object.assign({}, data, {
id: generateNewId(),
}),
};
this.operation = operation;
}
}
else {
const operation = this.operation;
assert(['create', 'update', action].includes(operation.action));
Object.assign(operation.data, data);
if (action && operation.action !== action) {
operation.action = action;
}
}
// 处理外键如果update的数据中有相应的外键其子对象上的动作应当被clean掉
// 并将sr传递到子组件上
for (const attr in data) {
if (attr === 'entityId') {
assert(data.entity, '设置entityId时请将entity也传入');
if (this.children[data.entity]) {
this.children[data.entity].clean();
this.passRsToChild(data.entity);
}
}
else if (this.schema[this.entity].attributes[attr]?.type === 'ref') {
const refKey = attr.slice(0, attr.length - 2);
if (this.children[refKey]) {
this.children[refKey].clean();
this.passRsToChild(refKey);
}
}
}
this.setDirty();
}
remove() {
assert(this.id);
const operation = {
id: generateNewId(),
action: 'remove',
data: {},
filter: {
id: this.id,
},
};
this.operation = operation;
// 此时应如何处理children除了clean之外似乎还应当unsetId没想清楚
this.setDirty();
}
setDirty() {
if (!this.operation) {
// 这种情况是下面的子结点setDirty引起的连锁设置
assert(this.id);
this.operation = {
id: generateNewId(),
action: 'update',
data: {},
filter: {
id: this.id,
}
};
}
super.setDirty();
}
composeOperations() {
if (this.dirty) {
const operation = this.operation && cloneDeep(this.operation);
if (operation) {
for (const ele in this.children) {
const ele2 = ele.includes(':') ? ele.slice(0, ele.indexOf(':')) : ele;
const child = this.children[ele];
const childOperations = child.composeOperations();
if (childOperations) {
if (child instanceof SingleNode) {
assert(childOperations.length === 1);
if (!operation.data[ele2]) {
Object.assign(operation.data, {
[ele2]: childOperations[0].operation,
});
}
else {
// 目前应该只允许一种情况就是父create子update
assert(operation.data[ele2].action === 'create' && childOperations[0].operation.action === 'update');
Object.assign(operation.data[ele2].data, childOperations[0].operation.data);
}
}
else {
assert(child instanceof ListNode);
const childOpers = childOperations.map(ele => ele.operation);
if (childOpers.length > 0) {
if (!operation.data[ele2]) {
operation.data[ele2] = childOpers;
}
else {
operation.data[ele2].push(...childOpers);
}
}
}
}
}
return [{
entity: this.entity,
operation,
}];
}
}
}
getProjection(withDecendants) {
const projection = super.getProjection();
if (projection && withDecendants) {
for (const k in this.children) {
if (projection[k] && process.env.NODE_ENV === 'development') {
console.warn(`父结点都定义了${k}路径上的projection和子结点产生冲突`);
}
if (!k.includes(MODI_NEXT_PATH_SUFFIX)) {
const rel = this.judgeRelation(k.includes(':') ? k.split(':')[0] : k);
if (rel === 2) {
const subProjection = this.children[k].getProjection(true);
Object.assign(projection, {
entity: 1,
entityId: 1,
[k]: subProjection,
});
}
else if (typeof rel === 'string') {
const subProjection = this.children[k].getProjection(true);
Object.assign(projection, {
[`${k}Id`]: 1,
[k]: subProjection,
});
}
else if (!k.endsWith('$$aggr')) {
const child = this.children[k];
assert(rel instanceof Array && child instanceof ListNode);
const subSelection = child.constructSelection();
const subEntity = child.getEntity();
Object.assign(projection, {
[k]: Object.assign(subSelection, {
$entity: subEntity,
})
});
}
}
}
}
return projection;
}
passRsToChild(k) {
/**
* 把返回的结果中的total和aggr相关的值下降到相关的子结点上去
*/
const projection = this.getProjection(true);
const [value] = this.cache.get(this.entity, {
data: projection,
filter: {
id: this.id,
},
});
const keys = k ? [k] : Object.keys(this.children || {});
for (const k of keys) {
const child = this.children[k];
const rel = this.judgeRelation(k);
if (rel === 2) {
if (value?.entityId) {
assert(child instanceof SingleNode);
assert(value.entity === child.getEntity());
child.saveRefreshResult({
[value.entityId]: this.sr[k] || {},
});
}
}
else if (typeof rel === 'string') {
if (value && value[`${k}Id`]) {
assert(child instanceof SingleNode);
assert(rel === child.getEntity());
child.saveRefreshResult({
[value[`${k}Id`]]: this.sr[k] || {},
});
}
}
else {
assert(rel instanceof Array);
assert(child instanceof ListNode);
// assert(this.sr![k]);
child.saveRefreshResult(this.sr[k] || {});
}
}
}
saveRefreshResult(data) {
const ids = Object.keys(data);
assert(ids.length === 1);
this.id = ids[0];
this.sr = data[ids[0]];
this.passRsToChild();
this.publish();
}
async refresh() {
// SingleNode如果是非根结点其id应该在第一次refresh的时候来确定
const projection = this.getProjection(true);
const filter = this.getFilter();
if (projection && filter) {
this.startLoading();
this.publish();
try {
await this.cache.refresh(this.entity, {
data: projection,
filter,
}, undefined, (result) => {
// 刷新后所有的更新都应当被丢弃子层上可能会自动建立了this.create动作 这里可能会有问题 by Xc 20230329
if (this.schema[this.entity].toModi) {
// 对于modi对象在此缓存modiIds
const rows = this.cache.get('modi', {
data: {
id: 1,
},
filter: {
entity: this.entity,
entityId: this.id,
iState: 'active',
}
});
if (rows.length > 0) {
this.modiIds = rows.map(ele => ele.id);
}
}
this.saveRefreshResult(result.data);
this.setFiltersAndSortedApplied();
this.endLoading();
//this.clean();
});
}
catch (err) {
this.endLoading();
this.publish();
throw err;
}
}
else {
// 不刷新也publish一下触发页面reRender不然有可能导致页面不进入formData
this.publish();
}
}
clean() {
if (this.dirty) {
this.operation = undefined;
for (const child in this.children) {
this.children[child].clean();
}
this.dirty = undefined;
this.publish();
}
}
getFilter() {
// 如果是新建等于没有filter
if (this.operation?.action === 'create') {
return;
}
// singleNode的filter可以优化权限的判断范围
let filter = {
id: this.id,
};
if (this.filters) {
filter = combineFilters(this.entity, this.schema, this.filters.map(ele => typeof ele.filter === 'function' ? ele.filter() : ele.filter).concat(filter));
}
return filter;
}
getIntrinsticFilters() {
return this.getFilter();
}
/**
* getParentFilter不能假设一定已经有数据只能根据当前filter的条件去构造
* @param childNode
* @param disableOperation
* @returns
*/
getParentFilter(childNode, ignoreNewParent) {
const value = this.getFreshValue();
if (value && value.$$createAt$$ === 1 && ignoreNewParent) {
return;
}
for (const key in this.children) {
if (childNode === this.children[key]) {
const rel = this.judgeRelation(key);
assert(rel instanceof Array && !key.endsWith('$$aggr'));
if (rel[1]) {
// 基于普通外键的一对多
if (value) {
return {
[rel[1]]: value.id,
};
}
const filter = this.getFilter();
if (filter) {
if (filter.id && Object.keys(filter).length === 1) {
return {
[rel[1]]: filter.id,
};
}
return {
[rel[1].slice(0, rel[1].length - 2)]: filter,
};
}
}
else {
// 基于entity/entityId的一对多
if (value) {
return {
entity: this.entity,
entityId: value.id,
};
}
const filter = this.getFilter();
if (filter) {
if (filter.id && Object.keys(filter).length === 1) {
return {
entity: this.entity,
entityId: filter.id,
};
}
return {
[this.entity]: filter,
};
}
}
}
}
return;
}
}
class VirtualNode extends Feature {
dirty;
executing;
loading = false;
children;
constructor(path, parent) {
super();
this.dirty = false;
this.executing = false;
this.children = {};
if (parent) {
parent.addChild(path, this);
}
}
getActiveModies(child) {
return;
}
setDirty() {
this.dirty = true;
this.publish();
}
setFiltersAndSortedApplied() {
for (const k in this.children) {
this.children[k].setFiltersAndSortedApplied();
}
}
addChild(path, child) {
// 规范virtualNode子结点的命名路径和类型entity的singleNode必须被命名为entity或entity:numberListNode必须被命名为entitys或entitys:number
assert(!this.children[path]);
this.children[path] = child;
if (child instanceof SingleNode || child instanceof ListNode) {
const entity = child.getEntity();
if (child instanceof SingleNode) {
assert(path === entity || path.startsWith(`${entity}:`), `oakPath「${path}」不符合命名规范,请以「${entity}:」为命名起始标识`);
}
else {
assert(path === `${entity}s` || path.startsWith(`${entity}s:`), `oakPath「${path}」不符合命名规范,请以「${entity}s:」为命名起始标识`);
}
}
}
getChild(path) {
return this.children[path];
}
getParent() {
return undefined;
}
destroy() {
for (const k in this.children) {
this.children[k].destroy();
}
}
getFreshValue() {
return undefined;
}
isDirty() {
return this.dirty;
}
async refresh() {
this.loading = true;
this.publish();
try {
if (Object.keys(this.children).length > 0) {
await Promise.all(Object.keys(this.children).map(ele => this.children[ele].refresh()));
}
this.loading = false;
this.publish();
}
catch (err) {
this.loading = false;
this.publish();
throw err;
}
}
composeOperations() {
/**
* 当一个virtualNode有多个子结点而这些子结点的前缀一致时标识这些子结点其实是指向同一个对象此时需要合并
*/
const operationss = [];
const operationDict = {};
for (const ele in this.children) {
const operation = this.children[ele].composeOperations();
if (operation) {
const idx = ele.indexOf(':') !== -1 ? ele.slice(0, ele.indexOf(':')) : ele;
if (operationDict[idx]) {
assert(false, '这种情况不应当再出现');
// 需要合并这两个子结点的动作 todo 两个子结点指向同一个对象,这种情况应当要消除
/* if (this.children[ele] instanceof SingleNode) {
// mergeOperationOper(this.children[ele].getEntity(), this.children[ele].getSchema(), operation[0], operationDict[idx][0]);
}
else {
console.warn('发生virtualNode上的list页面的动作merge请查看');
operationDict[idx].push(...operation);
} */
}
else {
operationDict[idx] = operation;
}
}
}
for (const k in operationDict) {
operationss.push(...operationDict[k]);
}
return operationss;
}
setExecuting(executing) {
this.executing = executing;
this.publish();
}
isExecuting() {
return this.executing;
}
isLoading() {
return this.loading;
}
clean() {
for (const ele in this.children) {
this.children[ele].clean();
}
this.dirty = false;
this.publish();
}
checkIfClean() {
for (const k in this.children) {
if (this.children[k].isDirty()) {
return;
}
}
this.dirty = false;
}
}
function analyzePath(path) {
const idx = path.lastIndexOf('.');
if (idx !== -1) {
return {
parent: path.slice(0, idx),
path: path.slice(idx + 1),
};
}
return {
path,
};
}
export class RunningTree extends Feature {
cache;
schema;
root;
relationAuth; // todo 用这个来帮助selection加上要取的权限相关的数据
constructor(cache, schema, relationAuth) {
super();
// this.aspectWrapper = aspectWrapper;
this.cache = cache;
this.schema = schema;
this.relationAuth = relationAuth;
this.root = {};
}
createNode(options) {
const { entity, pagination, path: fullPath, filters, sorters, projection, isList, id, actions, cascadeActions, getTotal, } = options;
let node;
const { parent, path } = analyzePath(fullPath);
const parentNode = parent ? this.findNode(parent) : undefined;
node = this.findNode(fullPath);
if (node) {
// 现在都允许结点不析构
if (entity) {
if (node instanceof ListNode) {
assert(isList && node.getEntity() === entity);
if (!node.getProjection() && projection) {
node.setProjection(projection);
if (filters) {
node.setNamedFilters(filters);
}
if (sorters) {
node.setNamedSorters(sorters);
}
if (pagination) {
node.setPagination(pagination, false); // 创建成功后会统一refresh
}
}
else if (projection) {
// 这里有一个例外是queryPanel这种和父结点共用此结点的抽象组件
// assert(false, `创建node时发现path[${fullPath}]已经存在有效的ListNod结点这种情况不应该存在`);
}
}
else {
assert(node instanceof SingleNode);
assert(!isList && node.getEntity() === entity);
if (!node.getProjection() && projection) {
node.setProjection(projection);
if (id) {
const id2 = node.getId();
assert(id === id2, `重用path[${fullPath}]上的singleNode时其上没有置有效id这种情况id应当由父结点设置`);
}
}
else {
// 目前只有一种情况合法即parentNode是list列表中的位置移动引起的重用
// assert(parentNode instanceof ListNode, `创建node时发现path[${fullPath}]已有有效的SingleNode结点本情况不应当存在`);
}
}
}
return node;
}
if (entity) {
if (isList) {
assert(!(parentNode instanceof ListNode));
// assert(projection, `页面没有定义投影「${path}」`);
node = new ListNode(entity, this.schema, this.cache, this.relationAuth, projection, parentNode, path, filters, sorters, getTotal, pagination, actions, cascadeActions);
}
else {
node = new SingleNode(entity, this.schema, this.cache, this.relationAuth, projection, parentNode, // 过编译
path, id, filters, actions, cascadeActions);
}
}
else {
assert(!parentNode || parentNode instanceof VirtualNode);
node = new VirtualNode(path, parentNode);
}
if (!parentNode) {
assert(!parent && !this.root[path]);
this.root[path] = node;
}
node.subscribe(() => {
this.publish(fullPath);
});
return node;
}
checkSingleNodeIsModiNode(node) {
const id = node.getId();
if (id) {
const modies = this.cache.get('modi', {
data: {
id: 1,
},
filter: {
targetEntity: node.getEntity(),
action: 'create',
data: {
id,
},
iState: 'active',
}
});
return modies.length > 0;
}
return false;
}
checkIsModiNode(path) {
if (!path.includes(MODI_NEXT_PATH_SUFFIX)) {
return false;
}
const node = this.findNode(path);
if (node instanceof SingleNode) {
return this.checkSingleNodeIsModiNode(node);
}
else {
assert(node instanceof ListNode);
const parent = node.getParent();
assert(parent instanceof SingleNode);
return this.checkSingleNodeIsModiNode(parent);
}
}
findNode(path) {
if (this.root[path]) {
return this.root[path];
}
const paths = path.split('.');
let node = this.root[paths[0]];
let iter = 1;
while (iter < paths.length && node) {
if (!node) {
return;
}
const childPath = paths[iter];
iter++;
node = node.getChild(childPath);
}
return node;
}
destroyNode(path) {
const node = this.findNode(path);
if (node) {
const childPath = path.slice(path.lastIndexOf('.') + 1);
const parent = node.getParent();
if (parent instanceof SingleNode) {
parent.removeChild(childPath);
}
else if (!parent) {
assert(this.root.hasOwnProperty(path));
unset(this.root, path);
}
node.destroy();
node.clearSubscribes();
}
}
getFreshValue(path) {
const node = this.findNode(path);
const paths = path.split('.');
const root = this.root[paths[0]];
const includeModi = path.includes(MODI_NEXT_PATH_SUFFIX);
if (node) {
this.cache.begin();
try {
assert(node instanceof ListNode || node instanceof SingleNode);
if (includeModi) {
const opers2 = node.getActiveModiOperations();
if (opers2) {
this.cache.redoOperation(opers2);
}
}
const opers = root?.composeOperations();
if (opers) {
this.cache.redoOperation(opers);
}
const value = node.getFreshValue();
this.cache.rollback();
return value;
}
catch (err) {
this.cache.rollback();
throw err;
}
}
}
isDirty(path) {
const node = this.findNode(path);
return node ? node.isDirty() : false;
}
addItem(path, data) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.addItem(data);
}
removeItem(path, id) {
const node = this.findNode(path);
assert(node instanceof ListNode);
node.removeItem(id);
}
updateItem(path, data, id, action) {
const node = this.findNode(path);
assert(node instanceof ListNode);
node.updateItem(data, id, action);
}
recoverItem(path, id) {
const node = this.findNode(path);
assert(node instanceof ListNode);
node.recoverItem(id);
}
resetItem(path, id) {
const node = this.findNode(path);
assert(node instanceof ListNode);
node.resetItem(id);
}
create(path, data) {
const node = this.findNode(path);
assert(node instanceof SingleNode);
node.create(data);
}
update(path, data, action) {
const node = this.findNode(path);
assert(node instanceof SingleNode);
node.update(data, action);
}
remove(path) {
const node = this.findNode(path);
assert(node instanceof SingleNode);
node.remove();
}
isCreation(path) {
const node = this.findNode(path);
assert(node instanceof SingleNode);
const oper = node.composeOperations();
return !!(oper && oper[0].operation.action === 'create');
}
isLoading(path) {
const node = this.findNode(path);
return node?.isLoading();
}
isLoadingMore(path) {
const node = this.findNode(path);
assert(!node || (node instanceof SingleNode || node instanceof ListNode));
return node?.isLoadingMore();
}
isExecuting(path) {
const node = this.findNode(path);
return node ? node.isExecuting() : false;
}
isListDescandent(path) {
const node = this.findNode(path);
let parent = node?.getParent();
while (parent) {
if (parent instanceof ListNode) {
return true;
}
parent = parent.getParent();
}
return false;
}
async refresh(path) {
/* if (path.includes(MODI_NEXT_PATH_SUFFIX)) {
return;
} */
const node = this.findNode(path);
if (!node?.isLoading()) {
if (node instanceof ListNode) {
await node.refresh(0, false);
}
else if (node) {
await node.refresh();
}
}
}
async loadMore(path) {
const node = this.findNode(path);
assert(node instanceof ListNode);
await node.loadMore();
}
getPagination(path) {
const node = this.findNode(path);
assert(node instanceof ListNode);
const pn = node.getPagination();
return {
...pn,
currentPage: pn.currentPage + 1,
};
}
setId(path, id) {
const node = this.findNode(path);
assert(node instanceof SingleNode);
return node.setId(id);
}
unsetId(path) {
const node = this.findNode(path);
assert(node instanceof SingleNode);
node.unsetId();
}
getId(path) {
const node = this.findNode(path);
assert(node instanceof SingleNode);
return node.getId();
}
getEntity(path) {
const node = this.findNode(path);
assert(node instanceof Node);
return node.getEntity();
}
setPageSize(path, pageSize) {
const node = this.findNode(path);
assert(node instanceof ListNode);
// 切换分页pageSize就重新设置
return node.setPagination({
pageSize,
currentPage: 1,
more: true,
});
}
setCurrentPage(path, currentPage) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.setCurrentPage(currentPage - 1);
}
getNamedFilters(path) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.getNamedFilters();
}
getNamedFilterByName(path, name) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.getNamedFilterByName(name);
}
setNamedFilters(path, filters, refresh = true) {
const node = this.findNode(path);
assert(node instanceof ListNode);
node.setNamedFilters(filters, refresh);
}
addNamedFilter(path, filter, refresh = false) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.addNamedFilter(filter, refresh);
}
removeNamedFilter(path, filter, refresh = false) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.removeNamedFilter(filter, refresh);
}
removeNamedFilterByName(path, name, refresh = false) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.removeNamedFilterByName(name, refresh);
}
getNamedSorters(path) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.getNamedSorters();
}
getNamedSorterByName(path, name) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.getNamedSorterByName(name);
}
setNamedSorters(path, sorters, refresh = true) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.setNamedSorters(sorters, refresh);
}
addNamedSorter(path, sorter, refresh = false) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.addNamedSorter(sorter, refresh);
}
removeNamedSorter(path, sorter, refresh = false) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.removeNamedSorter(sorter, refresh);
}
removeNamedSorterByName(path, name, refresh = false) {
const node = this.findNode(path);
assert(node instanceof ListNode);
return node.removeNamedSorterByName(name, refresh);
}
getIntrinsticFilters(path) {
const node = this.findNode(path);
assert(node instanceof ListNode || node instanceof SingleNode);
return node.getIntrinsticFilters();
}
tryExecute(path) {
const node = this.findNode(path);
const operations = node?.composeOperations();
if (operations && operations.length > 0) {
return this.cache.tryRedoOperations(operations);
}
return false;
}
getOperations(path) {
const node = this.findNode(path);
const operations = node?.composeOperations();
return operations;
}
async execute(path, action) {
const node = this.findNode(path);
if (action) {
if (node instanceof SingleNode) {
node.update({}, action);
}
else {
assert(node instanceof ListNode);
assert(false); // 对list的整体action等遇到了再实现
}
}
assert(node.isDirty());
node.setExecuting(true);
try {
const operations = node.composeOperations();
// 这里理论上virtualNode下面也可以有多个不同的entity的组件但实际中不应当出现这样的设计
if (operations.length > 0) {
const entities = uniq(operations.filter((ele) => !!ele).map((ele) => ele.entity));
assert(entities.length === 1);
const result = await this.cache.operate(entities[0], operations
.filter((ele) => !!ele)
.map((ele) => ele.operation), undefined, () => {
// 清空缓存
node.clean();
if (node instanceof SingleNode) {
assert(operations.length === 1);
// 这逻辑有点扯,页面自己决定后续逻辑 by Xc 20231108
// if (operations[0].operation.action === 'create') {
// // 如果是create动作给结点赋上id以保证页面数据的完整性
// const { id } = operations[0].operation.data as ED[keyof ED]['CreateSingle']['data'];
// node.setId(id);
// }
}
node.setExecuting(false);
});
return result;
}
node.clean();
node.setExecuting(false);
return { message: 'No Operation' };
}
catch (err) {
node.setExecuting(false);
throw err;
}
}
clean(path) {
const node = this.findNode(path);
node.clean();
const parent = node.getParent();
if (parent) {
parent.checkIfClean();
}
}
getRoot() {
return this.root;
}
}