Compare commits
40 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
2fd9a46011 | |
|
|
afe81e3fbb | |
|
|
0fc00a02b4 | |
|
|
a547f1d890 | |
|
|
b51889c9a9 | |
|
|
c82bf99fdb | |
|
|
5b5621216f | |
|
|
44d0df03cb | |
|
|
aa77a03f30 | |
|
|
ff0fcc8492 | |
|
|
e36cf430cf | |
|
|
a70f584e08 | |
|
|
b87d912388 | |
|
|
0f1b9168ac | |
|
|
17380fa8f9 | |
|
|
3b6ddbe317 | |
|
|
8cb729ad26 | |
|
|
7a5cfda18c | |
|
|
2dd4736907 | |
|
|
e59cbd5caf | |
|
|
f84ef1724d | |
|
|
7c8368c6d2 | |
|
|
4776865ece | |
|
|
8b892d95dd | |
|
|
0a9945ed2b | |
|
|
b9509af2f2 | |
|
|
4e8e6fee0b | |
|
|
bea065b1f3 | |
|
|
530d6b3c07 | |
|
|
3c74bf363f | |
|
|
43b2b04136 | |
|
|
0da1c9cac4 | |
|
|
ebe94ff393 | |
|
|
afdaab533e | |
|
|
e99818f10b | |
|
|
003c09cdd2 | |
|
|
5894c33dc9 | |
|
|
5bfd39ad69 | |
|
|
97e71c89be | |
|
|
a7b3b73536 |
|
|
@ -9,6 +9,7 @@ import { Context } from 'oak-domain/lib/types';
|
|||
export interface TreeStoreSelectOption extends SelectOption {
|
||||
nodeDict?: NodeDict;
|
||||
disableSubQueryHashjoin?: boolean;
|
||||
warnWhenAttributeMiss?: boolean;
|
||||
}
|
||||
export interface TreeStoreOperateOption extends OperateOption {
|
||||
}
|
||||
|
|
|
|||
368
es/store.js
368
es/store.js
|
|
@ -2,21 +2,22 @@ import { cloneDeep, get, groupBy, set, unset, uniqBy, uniq, differenceBy, inters
|
|||
import { assert } from 'oak-domain/lib/utils/assert';
|
||||
import { DeleteAtAttribute, CreateAtAttribute, UpdateAtAttribute } from "oak-domain/lib/types/Entity";
|
||||
import { EXPRESSION_PREFIX, SUB_QUERY_PREDICATE_KEYWORD } from 'oak-domain/lib/types/Demand';
|
||||
import { OakCongruentRowExists, OakException, OakRowUnexistedException } from 'oak-domain/lib/types/Exception';
|
||||
import { OakCongruentRowExists, OakException } from 'oak-domain/lib/types/Exception';
|
||||
import { isRefAttrNode } from 'oak-domain/lib/types/Demand';
|
||||
import { judgeRelation } from 'oak-domain/lib/store/relation';
|
||||
// Expression引入,下方声明改为any防止报错
|
||||
import { execOp, isExpression, opMultipleParams } from 'oak-domain/lib/types/Expression';
|
||||
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
||||
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
||||
import { CascadeStore, polishSelection } from 'oak-domain/lib/store/CascadeStore';
|
||||
import { SeqAttribute } from 'oak-domain/lib/types';
|
||||
import { combineFilters, getRelevantIds } from 'oak-domain/lib/store/filter';
|
||||
;
|
||||
;
|
||||
function obscurePass(value, option) {
|
||||
return !!(option?.obscure && value === undefined);
|
||||
}
|
||||
class OakExpressionUnresolvedException extends OakException {
|
||||
}
|
||||
function showWarningAttributeMiss(entity, attr) {
|
||||
console.warn(`attribute miss: entity: ${entity}, attr: ${attr}`);
|
||||
}
|
||||
;
|
||||
export default class TreeStore extends CascadeStore {
|
||||
store;
|
||||
|
|
@ -70,8 +71,12 @@ export default class TreeStore extends CascadeStore {
|
|||
resetInitialData(data, stat) {
|
||||
this.store = {};
|
||||
const now = Date.now();
|
||||
const schema = this.getSchema();
|
||||
for (const entity in data) {
|
||||
const { attributes } = this.getSchema()[entity];
|
||||
if (!schema[entity]) {
|
||||
throw new Error(`reset unknown entity:[${entity}]`);
|
||||
}
|
||||
const { attributes } = schema[entity];
|
||||
this.store[entity] = {};
|
||||
for (const row of data[entity]) {
|
||||
for (const key in attributes) {
|
||||
|
|
@ -106,7 +111,7 @@ export default class TreeStore extends CascadeStore {
|
|||
});
|
||||
}
|
||||
else {
|
||||
this.setMaxSeq(entity, row[SeqAttribute]);
|
||||
this.setMaxSeq(entity, row[SeqAttribute] + 1);
|
||||
}
|
||||
assert(row.id && !row.id.includes('.'));
|
||||
set(this.store, `${entity}.${row.id}.$current`, row);
|
||||
|
|
@ -142,12 +147,12 @@ export default class TreeStore extends CascadeStore {
|
|||
this.seq = {};
|
||||
}
|
||||
constructRow(node, context, option) {
|
||||
let data = cloneDeep(node.$current);
|
||||
if (context.getCurrentTxnId() && node.$txnId === context.getCurrentTxnId()) {
|
||||
if (!node.$next) {
|
||||
// 如果要求返回delete数据,返回带$$deleteAt$$的行
|
||||
if (option?.includedDeleted) {
|
||||
return Object.assign({}, data, {
|
||||
// bug fixed,这里如果是自己create再删除,data也是null
|
||||
if (node.$current && option?.includedDeleted) {
|
||||
return Object.assign({}, node.$current, {
|
||||
[DeleteAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
|
|
@ -155,19 +160,21 @@ export default class TreeStore extends CascadeStore {
|
|||
}
|
||||
else if (!node.$current) {
|
||||
// 本事务创建的,若在cache中$$createAt$$和$$updateAt$$置为1
|
||||
return Object.assign({}, data, node.$next, context instanceof SyncContext && {
|
||||
return Object.assign({}, node.$current, node.$next, context instanceof SyncContext && {
|
||||
[CreateAtAttribute]: 1,
|
||||
[UpdateAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// 本事务更新的,若在cache中$$updateAt$$置为1
|
||||
return Object.assign({}, data, node.$next, context instanceof SyncContext && {
|
||||
return Object.assign({}, node.$current, node.$next, context instanceof SyncContext && {
|
||||
[UpdateAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return {
|
||||
...node.$current,
|
||||
};
|
||||
}
|
||||
testFilterFns(node, nodeDict, exprResolveFns, fns) {
|
||||
const { self, otm, mto } = fns;
|
||||
|
|
@ -285,12 +292,16 @@ export default class TreeStore extends CascadeStore {
|
|||
* @param context
|
||||
* @returns
|
||||
*/
|
||||
translateExpressionNode(entity, expression, context, option) {
|
||||
translateExpressionNode(entity,
|
||||
// expression: Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']> | ExpressionConstant,
|
||||
expression, context, option) {
|
||||
if (isExpression(expression)) {
|
||||
const op = Object.keys(expression)[0];
|
||||
const option2 = expression[op];
|
||||
if (opMultipleParams(op)) {
|
||||
const paramsTranslated = option2.map(ele => this.translateExpressionNode(entity, ele, context, option2));
|
||||
const paramsTranslated = option2.map(
|
||||
// const paramsTranslated = (option2 as (Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']>)[]).map(
|
||||
ele => this.translateExpressionNode(entity, ele, context, option2));
|
||||
return (row, nodeDict) => {
|
||||
let later = false;
|
||||
let results = paramsTranslated.map((ele) => {
|
||||
|
|
@ -304,7 +315,7 @@ export default class TreeStore extends CascadeStore {
|
|||
return ele;
|
||||
});
|
||||
if (!later) {
|
||||
return execOp(op, results, option2.obscure);
|
||||
return execOp(op, results);
|
||||
}
|
||||
const laterCheckFn = (nodeDict2) => {
|
||||
results = results.map((ele) => {
|
||||
|
|
@ -317,7 +328,7 @@ export default class TreeStore extends CascadeStore {
|
|||
if (results.find(ele => typeof ele === 'function')) {
|
||||
return laterCheckFn;
|
||||
}
|
||||
return execOp(op, results, option && option.obscure);
|
||||
return execOp(op, results);
|
||||
};
|
||||
return laterCheckFn;
|
||||
};
|
||||
|
|
@ -337,12 +348,12 @@ export default class TreeStore extends CascadeStore {
|
|||
};
|
||||
return laterCheckFn;
|
||||
}
|
||||
return execOp(op, result, option2.obscure);
|
||||
return execOp(op, result);
|
||||
};
|
||||
}
|
||||
else {
|
||||
return () => {
|
||||
return execOp(op, paramsTranslated, option2.obscure);
|
||||
return execOp(op, paramsTranslated);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -352,7 +363,11 @@ export default class TreeStore extends CascadeStore {
|
|||
return (row, nodeDict) => {
|
||||
if (expression.hasOwnProperty('#attr')) {
|
||||
// 说明是本结点的属性;
|
||||
return row[expression['#attr']];
|
||||
const attr = row[expression['#attr']];
|
||||
if (attr === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, expression['#attr']);
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
else {
|
||||
assert(expression.hasOwnProperty('#refId'));
|
||||
|
|
@ -376,7 +391,9 @@ export default class TreeStore extends CascadeStore {
|
|||
return expression;
|
||||
}
|
||||
}
|
||||
translateExpression(entity, expression, context, option) {
|
||||
translateExpression(entity,
|
||||
// expression: Expression<keyof ED[T]['Schema']>,
|
||||
expression, context, option) {
|
||||
const expr = this.translateExpressionNode(entity, expression, context, option);
|
||||
return (row, nodeDict) => {
|
||||
if (typeof expr !== 'function') {
|
||||
|
|
@ -394,92 +411,109 @@ export default class TreeStore extends CascadeStore {
|
|||
const { $search } = filter;
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
for (const attr of attributes) {
|
||||
const { name } = attr;
|
||||
if (row && row[name] && (typeof row[name] === 'string' && row[name].includes($search) || obscurePass(row[name], option))) {
|
||||
return true;
|
||||
if (row) {
|
||||
for (const attr of attributes) {
|
||||
const { name } = attr;
|
||||
const value = row[name];
|
||||
if (value === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, name);
|
||||
}
|
||||
if (typeof value === 'string' && row[name].includes($search)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
translatePredicate(path, predicate, value, option) {
|
||||
translatePredicate(entity, path, predicate, value, option) {
|
||||
switch (predicate) {
|
||||
case '$gt': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data > value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data > value;
|
||||
};
|
||||
}
|
||||
case '$lt': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data < value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data < value;
|
||||
};
|
||||
}
|
||||
case '$gte': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data >= value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data >= value;
|
||||
};
|
||||
}
|
||||
case '$lte': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data <= value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data <= value;
|
||||
};
|
||||
}
|
||||
case '$eq': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data === value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data === value;
|
||||
};
|
||||
}
|
||||
case '$ne': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data !== value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data !== value;
|
||||
};
|
||||
}
|
||||
case '$between': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data >= value[0] && data <= value[1] || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data >= value[0] && data <= value[1];
|
||||
};
|
||||
}
|
||||
case '$mod': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return typeof data === 'number' && data % value[0] === value[1] || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return typeof data === 'number' && data % value[0] === value[1];
|
||||
};
|
||||
}
|
||||
case '$startsWith': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.startsWith(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['string'].includes(typeof data) && data.startsWith(value);
|
||||
};
|
||||
}
|
||||
case '$endsWith': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.endsWith(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['string'].includes(typeof data) && data.endsWith(value);
|
||||
};
|
||||
}
|
||||
case '$includes': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.includes(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['string'].includes(typeof data) && data.includes(value);
|
||||
};
|
||||
}
|
||||
case '$exists': {
|
||||
|
|
@ -487,11 +521,12 @@ export default class TreeStore extends CascadeStore {
|
|||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
if (value) {
|
||||
return ![null, undefined].includes(data) || obscurePass(data, option);
|
||||
return ![null, undefined].includes(data);
|
||||
}
|
||||
else {
|
||||
return [null, undefined].includes(data) || obscurePass(data, option);
|
||||
return [null, undefined].includes(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -499,14 +534,16 @@ export default class TreeStore extends CascadeStore {
|
|||
assert(value instanceof Array);
|
||||
return (row) => {
|
||||
const data = get(row, path);
|
||||
return value.includes(data) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return value.includes(data);
|
||||
};
|
||||
}
|
||||
case '$nin': {
|
||||
assert(value instanceof Array);
|
||||
return (row) => {
|
||||
const data = get(row, path);
|
||||
return !value.includes(data) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return !value.includes(data);
|
||||
};
|
||||
}
|
||||
case '$contains': {
|
||||
|
|
@ -514,12 +551,13 @@ export default class TreeStore extends CascadeStore {
|
|||
const array = value instanceof Array ? value : [value];
|
||||
return (row) => {
|
||||
const data = path ? get(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return differenceBy(array, data, (value) => {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}).length === 0 || obscurePass(data, option);
|
||||
}).length === 0;
|
||||
};
|
||||
}
|
||||
case '$overlaps': {
|
||||
|
|
@ -527,12 +565,28 @@ export default class TreeStore extends CascadeStore {
|
|||
const array = value instanceof Array ? value : [value];
|
||||
return (row) => {
|
||||
const data = path ? get(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return intersectionBy(array, data, (value) => {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}).length > 0 || obscurePass(data, option);
|
||||
}).length > 0;
|
||||
};
|
||||
}
|
||||
case '$length': {
|
||||
// json中的数组长度查询
|
||||
const length = value;
|
||||
return (row) => {
|
||||
const data = path ? get(row, path) : row;
|
||||
assert(data instanceof Array, '$length operator can only used on array attribute');
|
||||
if (typeof length === 'number') {
|
||||
return data.length === length;
|
||||
}
|
||||
else {
|
||||
const op = Object.keys(length)[0];
|
||||
return this.translatePredicate(entity, 'length', op, length[op], option)(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
|
@ -540,7 +594,7 @@ export default class TreeStore extends CascadeStore {
|
|||
}
|
||||
}
|
||||
}
|
||||
translateObjectPredicate(filter) {
|
||||
translateObjectPredicate(entity, filter) {
|
||||
const fns = [];
|
||||
const translatePredicateInner = (p, path, fns2) => {
|
||||
if (p instanceof Array) {
|
||||
|
|
@ -548,7 +602,7 @@ export default class TreeStore extends CascadeStore {
|
|||
const path2 = `${path}[${idx}]`;
|
||||
if (typeof ele !== 'object') {
|
||||
if (![null, undefined].includes(ele)) {
|
||||
fns2.push(this.translatePredicate(path2, '$eq', ele));
|
||||
fns2.push(this.translatePredicate(entity, path2, '$eq', ele));
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -575,13 +629,13 @@ export default class TreeStore extends CascadeStore {
|
|||
}
|
||||
else if (attr.startsWith('$')) {
|
||||
assert(Object.keys(p).length === 1);
|
||||
fns2.push(this.translatePredicate(path, attr, p[attr]));
|
||||
fns2.push(this.translatePredicate(entity, path, attr, p[attr]));
|
||||
}
|
||||
else {
|
||||
const attr2 = attr.startsWith('.') ? attr.slice(1) : attr;
|
||||
const path2 = path ? `${path}.${attr2}` : attr2;
|
||||
if (typeof p[attr] !== 'object') {
|
||||
fns2.push(this.translatePredicate(path2, '$eq', p[attr]));
|
||||
fns2.push(this.translatePredicate(entity, path2, '$eq', p[attr]));
|
||||
}
|
||||
else {
|
||||
translatePredicateInner(p[attr], path2, fns2);
|
||||
|
|
@ -601,15 +655,18 @@ export default class TreeStore extends CascadeStore {
|
|||
};
|
||||
}
|
||||
translateAttribute(entity, filter, attr, context, option) {
|
||||
// 如果是模糊查询且该属性为undefined,说明没取到,返回true
|
||||
function obscurePassLocal(row) {
|
||||
return obscurePass(row[attr], option);
|
||||
}
|
||||
if (!['object', 'array'].includes(this.getSchema()[entity].attributes[attr]?.type)) {
|
||||
if (typeof filter !== 'object') {
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
return row ? row[attr] === filter || obscurePassLocal(row) : false;
|
||||
if (row) {
|
||||
const value = row[attr];
|
||||
if (value === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, attr);
|
||||
}
|
||||
return value === filter;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
else {
|
||||
|
|
@ -619,7 +676,7 @@ export default class TreeStore extends CascadeStore {
|
|||
throw new Error('子查询已经改用一对多的外键连接方式');
|
||||
}
|
||||
else {
|
||||
const fn = this.translatePredicate(attr, predicate, filter[predicate], option);
|
||||
const fn = this.translatePredicate(entity, attr, predicate, filter[predicate], option);
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
if (!row) {
|
||||
|
|
@ -644,13 +701,13 @@ export default class TreeStore extends CascadeStore {
|
|||
};
|
||||
}
|
||||
else {
|
||||
const fn = this.translateObjectPredicate(filter);
|
||||
const fn = this.translateObjectPredicate(entity, filter);
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return fn(row[attr]) || obscurePassLocal(row);
|
||||
return fn(row[attr]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -703,27 +760,11 @@ export default class TreeStore extends CascadeStore {
|
|||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
if (obscurePass(row.entity, option) || obscurePass(row.entityId, option)) {
|
||||
return true;
|
||||
}
|
||||
if (row.entityId === undefined || row.entity === undefined) {
|
||||
// 这个assert在count的情况下不满足 by Xc 20240205
|
||||
// assert(typeof projection[attr] === 'object');
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上的entity/entityId不能确定值,可能会影响判定结果`);
|
||||
}
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
if (option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, 'entity/entityId');
|
||||
}
|
||||
throw new OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
}
|
||||
if (row.entity !== attr) {
|
||||
return false;
|
||||
|
|
@ -733,9 +774,6 @@ export default class TreeStore extends CascadeStore {
|
|||
}
|
||||
const node2 = get(this.store, `${attr}.${row.entityId}`);
|
||||
if (!node2) {
|
||||
if (option?.obscure) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return filterFn(node2, nodeDict, exprResolveFns);
|
||||
|
|
@ -749,40 +787,18 @@ export default class TreeStore extends CascadeStore {
|
|||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
if (obscurePass(row[`${attr}Id`], option)) {
|
||||
return true;
|
||||
}
|
||||
if (row[`${attr}Id`]) {
|
||||
const node2 = get(this.store, `${relation}.${row[`${attr}Id`]}`);
|
||||
if (!node2) {
|
||||
if (option?.obscure) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return filterFn(node2, nodeDict, exprResolveFns);
|
||||
}
|
||||
if (row[`${attr}Id`] === undefined) {
|
||||
// 说明一对多的外键没有取出来,需要抛出RowUnexists异常
|
||||
// 这个assert在count的情况下不满足 by Xc 20240205
|
||||
// assert(typeof projection[attr] === 'object');
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上的${attr}Id不能确定值,可能会影响判定结果`);
|
||||
}
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
if (option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, `${attr}Id`);
|
||||
}
|
||||
throw new OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
}
|
||||
assert(row[`${attr}Id`] === null);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
|
@ -790,13 +806,6 @@ export default class TreeStore extends CascadeStore {
|
|||
// 一对多的子查询
|
||||
const [otmEntity, otmForeignKey] = relation;
|
||||
const predicate = filter[attr][SUB_QUERY_PREDICATE_KEYWORD] || 'in';
|
||||
if (option?.obscure) {
|
||||
// 如果是obscure,则返回的集合中有没有都不能否决“可能有”或“并不全部是”,所以可以直接返回true
|
||||
if (['in', 'not all'].includes(predicate)) {
|
||||
self.push(() => true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const fk = otmForeignKey || 'entityId';
|
||||
const otmProjection = {
|
||||
id: 1,
|
||||
|
|
@ -805,8 +814,8 @@ export default class TreeStore extends CascadeStore {
|
|||
/**
|
||||
* in代表外键连接后至少有一行数据
|
||||
* not in代表外键连接后一行也不能有
|
||||
* all代表反外键连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反外键连接条件的至少有一行
|
||||
* all代表反连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反连接条件的至少有一行
|
||||
*
|
||||
* 此时还没有确定父行,只有查询中明确带有id的查询可以先执行,否则不执行,暂先这个逻辑 by Xc 20230725
|
||||
*/
|
||||
|
|
@ -819,31 +828,31 @@ export default class TreeStore extends CascadeStore {
|
|||
/**
|
||||
* in代表外键连接后至少有一行数据
|
||||
* not in代表外键连接后一行也不能有
|
||||
* all代表反外键连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反外键连接条件的至少有一行
|
||||
* all代表反连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反连接条件的至少有一行
|
||||
*/
|
||||
const otmFilter = !otmForeignKey ? Object.assign({
|
||||
const otmOriginFilter = !otmForeignKey ? Object.assign({
|
||||
entity,
|
||||
}, filter[attr]) : cloneDeep(filter[attr]);
|
||||
if (['noobscuret in', 'in'].includes(predicate)) {
|
||||
Object.assign(otmFilter, {
|
||||
}, cloneDeep(filter[attr])) : cloneDeep(filter[attr]);
|
||||
const otmFilter = ['not in', 'in'].includes(predicate) ? combineFilters(otmEntity, this.getSchema(), [
|
||||
otmOriginFilter, {
|
||||
[fk]: row.id,
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.assign(otmFilter, {
|
||||
[fk]: {
|
||||
$ne: row.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
const option2 = Object.assign({}, option, { nodeDict, dontCollect: true });
|
||||
}
|
||||
]) : {
|
||||
$not: otmOriginFilter,
|
||||
[fk]: row.id,
|
||||
};
|
||||
const subQuerySet = (this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: otmFilter,
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, context, option2)).map((ele) => {
|
||||
}, context, {
|
||||
...option,
|
||||
nodeDict,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
})).map((ele) => {
|
||||
return (ele)[fk];
|
||||
});
|
||||
switch (predicate) {
|
||||
|
|
@ -862,28 +871,28 @@ export default class TreeStore extends CascadeStore {
|
|||
});
|
||||
};
|
||||
if (filter.id && typeof filter.id === 'string') {
|
||||
const otmFilter = !otmForeignKey ? Object.assign({
|
||||
const otmOriginFilter = !otmForeignKey ? Object.assign({
|
||||
entity,
|
||||
}, filter[attr]) : cloneDeep(filter[attr]);
|
||||
if (['not in', 'in'].includes(predicate)) {
|
||||
Object.assign(otmFilter, {
|
||||
}, cloneDeep(filter[attr])) : cloneDeep(filter[attr]);
|
||||
const otmFilter = ['not in', 'in'].includes(predicate) ? combineFilters(otmEntity, this.getSchema(), [
|
||||
otmOriginFilter, {
|
||||
[fk]: filter.id,
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.assign(otmFilter, {
|
||||
[fk]: {
|
||||
$ne: filter.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]) : {
|
||||
$not: otmOriginFilter,
|
||||
[fk]: filter.id,
|
||||
};
|
||||
try {
|
||||
const subQuerySet = (this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: otmFilter,
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, context, { dontCollect: true })).map((ele) => {
|
||||
}, context, {
|
||||
...option,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
})).map((ele) => {
|
||||
return (ele)[fk];
|
||||
});
|
||||
self.push((node) => {
|
||||
|
|
@ -919,12 +928,15 @@ export default class TreeStore extends CascadeStore {
|
|||
/**
|
||||
* 尝试用hashjoin将内表数组取出,因为memory中的表都不会太大,且用不了索引(未来优化了可能可以用id直接取值),因而用hash应当会更快
|
||||
*/
|
||||
const option2 = Object.assign({}, option, { dontCollect: true });
|
||||
try {
|
||||
const subQueryRows = this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: filter[attr],
|
||||
}, context, option2);
|
||||
}, context, {
|
||||
...option,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
});
|
||||
const buckets = groupBy(subQueryRows, fk);
|
||||
otm.push((node, nodeDict) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
|
|
@ -1355,7 +1367,6 @@ export default class TreeStore extends CascadeStore {
|
|||
}
|
||||
// 先计算projection,formResult只处理abjoint的行,不需要考虑expression和一对多多对一关系
|
||||
let rows2 = [];
|
||||
const incompletedRowIds = [];
|
||||
const { data: projection } = selection;
|
||||
for (const row of rows) {
|
||||
const result = {};
|
||||
|
|
@ -1363,7 +1374,9 @@ export default class TreeStore extends CascadeStore {
|
|||
const rel = this.judgeRelation(entity, attr);
|
||||
if (rel === 1) {
|
||||
if (row[attr] === undefined) {
|
||||
incompletedRowIds.push(row.id);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上的属性${attr}缺失,可能会影响上层使用结果,请确定`);
|
||||
}
|
||||
// break;
|
||||
}
|
||||
else if (typeof projection[attr] === 'number') {
|
||||
|
|
@ -1426,29 +1439,6 @@ export default class TreeStore extends CascadeStore {
|
|||
}
|
||||
rows2.push(result);
|
||||
}
|
||||
if (incompletedRowIds.length > 0) {
|
||||
// 如果有缺失属性的行,则报OakRowUnexistedException错误
|
||||
// fixed: 这里不报了。按约定框架应当保证取到要访问的属性
|
||||
// fixed: 和外键缺失一样,还是报,上层在知道框架会保证取到的情况下用allowMiss忽略此错误
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上有属性缺失,可能会影响上层使用结果,请确定`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: {
|
||||
$in: incompletedRowIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
}
|
||||
}
|
||||
// 再计算sorter
|
||||
if (sorter) {
|
||||
const sorterFn = this.translateSorter(entity, sorter, context, option);
|
||||
|
|
@ -1493,7 +1483,7 @@ export default class TreeStore extends CascadeStore {
|
|||
else {
|
||||
assert([0, 1].includes(rel));
|
||||
result2[k] = row2[k];
|
||||
assert(['string', 'number', 'boolean'].includes(typeof row2[k]));
|
||||
assert(['string', 'number', 'boolean'].includes(typeof row2[k]) || row2[k] === null);
|
||||
key += `${row2[k]}`;
|
||||
values.push(row2[k]);
|
||||
}
|
||||
|
|
@ -1591,8 +1581,9 @@ export default class TreeStore extends CascadeStore {
|
|||
result[op] = 0;
|
||||
const data = distinct ? uniq(results[op]) : results[op];
|
||||
data.forEach((ele) => {
|
||||
assert(typeof ele === 'number', '只有number类型的属性才可以计算sum');
|
||||
result[op] += ele;
|
||||
if (typeof ele === 'number') {
|
||||
result[op] += ele;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (op.startsWith('#count')) {
|
||||
|
|
@ -1607,11 +1598,14 @@ export default class TreeStore extends CascadeStore {
|
|||
else if (op.startsWith('#avg')) {
|
||||
result[op] = 0;
|
||||
const data = (distinct ? uniq(results[op]) : results[op]).filter(ele => ![undefined, null].includes(ele));
|
||||
let count = 0;
|
||||
data.forEach((ele) => {
|
||||
assert(typeof ele === 'number', '只有number类型的属性才可以计算avg');
|
||||
result[op] += ele;
|
||||
if (typeof ele === 'number') {
|
||||
result[op] += ele;
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
result[op] = result[op] / data.length;
|
||||
result[op] = result[op] / count;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -1684,6 +1678,7 @@ export default class TreeStore extends CascadeStore {
|
|||
indexFrom,
|
||||
count,
|
||||
};
|
||||
polishSelection(this.getSchema(), entity, selection);
|
||||
const result = await this.cascadeSelectAsync(entity, selection, context, Object.assign({}, option, {
|
||||
dontCollect: true,
|
||||
}));
|
||||
|
|
@ -1882,7 +1877,7 @@ export default class TreeStore extends CascadeStore {
|
|||
if (d instanceof Array) {
|
||||
for (const dd of d) {
|
||||
assert(dd[UpdateAtAttribute], `获取的${e}对象数据没有update时间戳`);
|
||||
if (!this.store[e][dd.id]) {
|
||||
if (!this.store[e]?.[dd.id]) {
|
||||
// 有可能后台通过socket等途径先把数据推到了前台,这里直接忽略就行了
|
||||
this.updateAbjointRow(e, {
|
||||
id: 'dummy',
|
||||
|
|
@ -1930,16 +1925,19 @@ export default class TreeStore extends CascadeStore {
|
|||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'create');
|
||||
}
|
||||
else if (this.store[entity][id].$current?.[UpdateAtAttribute] <= d[entity][id][UpdateAtAttribute]) {
|
||||
this.updateAbjointRow(entity, {
|
||||
id: 'dummy',
|
||||
action: 'update',
|
||||
data: d[entity][id],
|
||||
filter: {
|
||||
id,
|
||||
},
|
||||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'update');
|
||||
else if (this.store[entity]?.[id]) {
|
||||
const row = this.constructRow(this.store[entity][id], context);
|
||||
if (row[UpdateAtAttribute] <= d[entity][id][UpdateAtAttribute]) {
|
||||
this.updateAbjointRow(entity, {
|
||||
id: 'dummy',
|
||||
action: 'update',
|
||||
data: d[entity][id],
|
||||
filter: {
|
||||
id,
|
||||
},
|
||||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'update');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Context } from 'oak-domain/lib/types';
|
|||
export interface TreeStoreSelectOption extends SelectOption {
|
||||
nodeDict?: NodeDict;
|
||||
disableSubQueryHashjoin?: boolean;
|
||||
warnWhenAttributeMiss?: boolean;
|
||||
}
|
||||
export interface TreeStoreOperateOption extends OperateOption {
|
||||
}
|
||||
|
|
|
|||
364
lib/store.js
364
lib/store.js
|
|
@ -7,6 +7,7 @@ const Demand_1 = require("oak-domain/lib/types/Demand");
|
|||
const Exception_1 = require("oak-domain/lib/types/Exception");
|
||||
const Demand_2 = require("oak-domain/lib/types/Demand");
|
||||
const relation_1 = require("oak-domain/lib/store/relation");
|
||||
// Expression引入,下方声明改为any防止报错
|
||||
const Expression_1 = require("oak-domain/lib/types/Expression");
|
||||
const SyncRowStore_1 = require("oak-domain/lib/store/SyncRowStore");
|
||||
const CascadeStore_1 = require("oak-domain/lib/store/CascadeStore");
|
||||
|
|
@ -14,11 +15,11 @@ const types_1 = require("oak-domain/lib/types");
|
|||
const filter_1 = require("oak-domain/lib/store/filter");
|
||||
;
|
||||
;
|
||||
function obscurePass(value, option) {
|
||||
return !!(option?.obscure && value === undefined);
|
||||
}
|
||||
class OakExpressionUnresolvedException extends Exception_1.OakException {
|
||||
}
|
||||
function showWarningAttributeMiss(entity, attr) {
|
||||
console.warn(`attribute miss: entity: ${entity}, attr: ${attr}`);
|
||||
}
|
||||
;
|
||||
class TreeStore extends CascadeStore_1.CascadeStore {
|
||||
store;
|
||||
|
|
@ -72,8 +73,12 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
resetInitialData(data, stat) {
|
||||
this.store = {};
|
||||
const now = Date.now();
|
||||
const schema = this.getSchema();
|
||||
for (const entity in data) {
|
||||
const { attributes } = this.getSchema()[entity];
|
||||
if (!schema[entity]) {
|
||||
throw new Error(`reset unknown entity:[${entity}]`);
|
||||
}
|
||||
const { attributes } = schema[entity];
|
||||
this.store[entity] = {};
|
||||
for (const row of data[entity]) {
|
||||
for (const key in attributes) {
|
||||
|
|
@ -108,7 +113,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
});
|
||||
}
|
||||
else {
|
||||
this.setMaxSeq(entity, row[types_1.SeqAttribute]);
|
||||
this.setMaxSeq(entity, row[types_1.SeqAttribute] + 1);
|
||||
}
|
||||
(0, assert_1.assert)(row.id && !row.id.includes('.'));
|
||||
(0, lodash_1.set)(this.store, `${entity}.${row.id}.$current`, row);
|
||||
|
|
@ -144,12 +149,12 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
this.seq = {};
|
||||
}
|
||||
constructRow(node, context, option) {
|
||||
let data = (0, lodash_1.cloneDeep)(node.$current);
|
||||
if (context.getCurrentTxnId() && node.$txnId === context.getCurrentTxnId()) {
|
||||
if (!node.$next) {
|
||||
// 如果要求返回delete数据,返回带$$deleteAt$$的行
|
||||
if (option?.includedDeleted) {
|
||||
return Object.assign({}, data, {
|
||||
// bug fixed,这里如果是自己create再删除,data也是null
|
||||
if (node.$current && option?.includedDeleted) {
|
||||
return Object.assign({}, node.$current, {
|
||||
[Entity_1.DeleteAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
|
|
@ -157,19 +162,21 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
}
|
||||
else if (!node.$current) {
|
||||
// 本事务创建的,若在cache中$$createAt$$和$$updateAt$$置为1
|
||||
return Object.assign({}, data, node.$next, context instanceof SyncRowStore_1.SyncContext && {
|
||||
return Object.assign({}, node.$current, node.$next, context instanceof SyncRowStore_1.SyncContext && {
|
||||
[Entity_1.CreateAtAttribute]: 1,
|
||||
[Entity_1.UpdateAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// 本事务更新的,若在cache中$$updateAt$$置为1
|
||||
return Object.assign({}, data, node.$next, context instanceof SyncRowStore_1.SyncContext && {
|
||||
return Object.assign({}, node.$current, node.$next, context instanceof SyncRowStore_1.SyncContext && {
|
||||
[Entity_1.UpdateAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return {
|
||||
...node.$current,
|
||||
};
|
||||
}
|
||||
testFilterFns(node, nodeDict, exprResolveFns, fns) {
|
||||
const { self, otm, mto } = fns;
|
||||
|
|
@ -287,12 +294,16 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
* @param context
|
||||
* @returns
|
||||
*/
|
||||
translateExpressionNode(entity, expression, context, option) {
|
||||
translateExpressionNode(entity,
|
||||
// expression: Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']> | ExpressionConstant,
|
||||
expression, context, option) {
|
||||
if ((0, Expression_1.isExpression)(expression)) {
|
||||
const op = Object.keys(expression)[0];
|
||||
const option2 = expression[op];
|
||||
if ((0, Expression_1.opMultipleParams)(op)) {
|
||||
const paramsTranslated = option2.map(ele => this.translateExpressionNode(entity, ele, context, option2));
|
||||
const paramsTranslated = option2.map(
|
||||
// const paramsTranslated = (option2 as (Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']>)[]).map(
|
||||
ele => this.translateExpressionNode(entity, ele, context, option2));
|
||||
return (row, nodeDict) => {
|
||||
let later = false;
|
||||
let results = paramsTranslated.map((ele) => {
|
||||
|
|
@ -306,7 +317,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
return ele;
|
||||
});
|
||||
if (!later) {
|
||||
return (0, Expression_1.execOp)(op, results, option2.obscure);
|
||||
return (0, Expression_1.execOp)(op, results);
|
||||
}
|
||||
const laterCheckFn = (nodeDict2) => {
|
||||
results = results.map((ele) => {
|
||||
|
|
@ -319,7 +330,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
if (results.find(ele => typeof ele === 'function')) {
|
||||
return laterCheckFn;
|
||||
}
|
||||
return (0, Expression_1.execOp)(op, results, option && option.obscure);
|
||||
return (0, Expression_1.execOp)(op, results);
|
||||
};
|
||||
return laterCheckFn;
|
||||
};
|
||||
|
|
@ -339,12 +350,12 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
};
|
||||
return laterCheckFn;
|
||||
}
|
||||
return (0, Expression_1.execOp)(op, result, option2.obscure);
|
||||
return (0, Expression_1.execOp)(op, result);
|
||||
};
|
||||
}
|
||||
else {
|
||||
return () => {
|
||||
return (0, Expression_1.execOp)(op, paramsTranslated, option2.obscure);
|
||||
return (0, Expression_1.execOp)(op, paramsTranslated);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -354,7 +365,11 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
return (row, nodeDict) => {
|
||||
if (expression.hasOwnProperty('#attr')) {
|
||||
// 说明是本结点的属性;
|
||||
return row[expression['#attr']];
|
||||
const attr = row[expression['#attr']];
|
||||
if (attr === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, expression['#attr']);
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
else {
|
||||
(0, assert_1.assert)(expression.hasOwnProperty('#refId'));
|
||||
|
|
@ -378,7 +393,9 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
return expression;
|
||||
}
|
||||
}
|
||||
translateExpression(entity, expression, context, option) {
|
||||
translateExpression(entity,
|
||||
// expression: Expression<keyof ED[T]['Schema']>,
|
||||
expression, context, option) {
|
||||
const expr = this.translateExpressionNode(entity, expression, context, option);
|
||||
return (row, nodeDict) => {
|
||||
if (typeof expr !== 'function') {
|
||||
|
|
@ -396,92 +413,109 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
const { $search } = filter;
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
for (const attr of attributes) {
|
||||
const { name } = attr;
|
||||
if (row && row[name] && (typeof row[name] === 'string' && row[name].includes($search) || obscurePass(row[name], option))) {
|
||||
return true;
|
||||
if (row) {
|
||||
for (const attr of attributes) {
|
||||
const { name } = attr;
|
||||
const value = row[name];
|
||||
if (value === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, name);
|
||||
}
|
||||
if (typeof value === 'string' && row[name].includes($search)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
translatePredicate(path, predicate, value, option) {
|
||||
translatePredicate(entity, path, predicate, value, option) {
|
||||
switch (predicate) {
|
||||
case '$gt': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data > value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data > value;
|
||||
};
|
||||
}
|
||||
case '$lt': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data < value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data < value;
|
||||
};
|
||||
}
|
||||
case '$gte': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data >= value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data >= value;
|
||||
};
|
||||
}
|
||||
case '$lte': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data <= value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data <= value;
|
||||
};
|
||||
}
|
||||
case '$eq': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data === value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data === value;
|
||||
};
|
||||
}
|
||||
case '$ne': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data !== value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data !== value;
|
||||
};
|
||||
}
|
||||
case '$between': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data >= value[0] && data <= value[1] || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data >= value[0] && data <= value[1];
|
||||
};
|
||||
}
|
||||
case '$mod': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return typeof data === 'number' && data % value[0] === value[1] || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return typeof data === 'number' && data % value[0] === value[1];
|
||||
};
|
||||
}
|
||||
case '$startsWith': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.startsWith(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['string'].includes(typeof data) && data.startsWith(value);
|
||||
};
|
||||
}
|
||||
case '$endsWith': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.endsWith(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['string'].includes(typeof data) && data.endsWith(value);
|
||||
};
|
||||
}
|
||||
case '$includes': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.includes(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return ['string'].includes(typeof data) && data.includes(value);
|
||||
};
|
||||
}
|
||||
case '$exists': {
|
||||
|
|
@ -489,11 +523,12 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
if (value) {
|
||||
return ![null, undefined].includes(data) || obscurePass(data, option);
|
||||
return ![null, undefined].includes(data);
|
||||
}
|
||||
else {
|
||||
return [null, undefined].includes(data) || obscurePass(data, option);
|
||||
return [null, undefined].includes(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -501,14 +536,16 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
(0, assert_1.assert)(value instanceof Array);
|
||||
return (row) => {
|
||||
const data = (0, lodash_1.get)(row, path);
|
||||
return value.includes(data) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return value.includes(data);
|
||||
};
|
||||
}
|
||||
case '$nin': {
|
||||
(0, assert_1.assert)(value instanceof Array);
|
||||
return (row) => {
|
||||
const data = (0, lodash_1.get)(row, path);
|
||||
return !value.includes(data) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return !value.includes(data);
|
||||
};
|
||||
}
|
||||
case '$contains': {
|
||||
|
|
@ -516,12 +553,13 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
const array = value instanceof Array ? value : [value];
|
||||
return (row) => {
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return (0, lodash_1.differenceBy)(array, data, (value) => {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}).length === 0 || obscurePass(data, option);
|
||||
}).length === 0;
|
||||
};
|
||||
}
|
||||
case '$overlaps': {
|
||||
|
|
@ -529,12 +567,28 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
const array = value instanceof Array ? value : [value];
|
||||
return (row) => {
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss(entity, path);
|
||||
return (0, lodash_1.intersectionBy)(array, data, (value) => {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}).length > 0 || obscurePass(data, option);
|
||||
}).length > 0;
|
||||
};
|
||||
}
|
||||
case '$length': {
|
||||
// json中的数组长度查询
|
||||
const length = value;
|
||||
return (row) => {
|
||||
const data = path ? (0, lodash_1.get)(row, path) : row;
|
||||
(0, assert_1.assert)(data instanceof Array, '$length operator can only used on array attribute');
|
||||
if (typeof length === 'number') {
|
||||
return data.length === length;
|
||||
}
|
||||
else {
|
||||
const op = Object.keys(length)[0];
|
||||
return this.translatePredicate(entity, 'length', op, length[op], option)(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
|
@ -542,7 +596,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
}
|
||||
}
|
||||
}
|
||||
translateObjectPredicate(filter) {
|
||||
translateObjectPredicate(entity, filter) {
|
||||
const fns = [];
|
||||
const translatePredicateInner = (p, path, fns2) => {
|
||||
if (p instanceof Array) {
|
||||
|
|
@ -550,7 +604,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
const path2 = `${path}[${idx}]`;
|
||||
if (typeof ele !== 'object') {
|
||||
if (![null, undefined].includes(ele)) {
|
||||
fns2.push(this.translatePredicate(path2, '$eq', ele));
|
||||
fns2.push(this.translatePredicate(entity, path2, '$eq', ele));
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -577,13 +631,13 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
}
|
||||
else if (attr.startsWith('$')) {
|
||||
(0, assert_1.assert)(Object.keys(p).length === 1);
|
||||
fns2.push(this.translatePredicate(path, attr, p[attr]));
|
||||
fns2.push(this.translatePredicate(entity, path, attr, p[attr]));
|
||||
}
|
||||
else {
|
||||
const attr2 = attr.startsWith('.') ? attr.slice(1) : attr;
|
||||
const path2 = path ? `${path}.${attr2}` : attr2;
|
||||
if (typeof p[attr] !== 'object') {
|
||||
fns2.push(this.translatePredicate(path2, '$eq', p[attr]));
|
||||
fns2.push(this.translatePredicate(entity, path2, '$eq', p[attr]));
|
||||
}
|
||||
else {
|
||||
translatePredicateInner(p[attr], path2, fns2);
|
||||
|
|
@ -603,15 +657,18 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
};
|
||||
}
|
||||
translateAttribute(entity, filter, attr, context, option) {
|
||||
// 如果是模糊查询且该属性为undefined,说明没取到,返回true
|
||||
function obscurePassLocal(row) {
|
||||
return obscurePass(row[attr], option);
|
||||
}
|
||||
if (!['object', 'array'].includes(this.getSchema()[entity].attributes[attr]?.type)) {
|
||||
if (typeof filter !== 'object') {
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
return row ? row[attr] === filter || obscurePassLocal(row) : false;
|
||||
if (row) {
|
||||
const value = row[attr];
|
||||
if (value === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, attr);
|
||||
}
|
||||
return value === filter;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
else {
|
||||
|
|
@ -621,7 +678,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
throw new Error('子查询已经改用一对多的外键连接方式');
|
||||
}
|
||||
else {
|
||||
const fn = this.translatePredicate(attr, predicate, filter[predicate], option);
|
||||
const fn = this.translatePredicate(entity, attr, predicate, filter[predicate], option);
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
if (!row) {
|
||||
|
|
@ -646,13 +703,13 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
};
|
||||
}
|
||||
else {
|
||||
const fn = this.translateObjectPredicate(filter);
|
||||
const fn = this.translateObjectPredicate(entity, filter);
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return fn(row[attr]) || obscurePassLocal(row);
|
||||
return fn(row[attr]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -705,27 +762,11 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
if (obscurePass(row.entity, option) || obscurePass(row.entityId, option)) {
|
||||
return true;
|
||||
}
|
||||
if (row.entityId === undefined || row.entity === undefined) {
|
||||
// 这个assert在count的情况下不满足 by Xc 20240205
|
||||
// assert(typeof projection[attr] === 'object');
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上的entity/entityId不能确定值,可能会影响判定结果`);
|
||||
}
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
if (option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, 'entity/entityId');
|
||||
}
|
||||
throw new Exception_1.OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
}
|
||||
if (row.entity !== attr) {
|
||||
return false;
|
||||
|
|
@ -735,9 +776,6 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
}
|
||||
const node2 = (0, lodash_1.get)(this.store, `${attr}.${row.entityId}`);
|
||||
if (!node2) {
|
||||
if (option?.obscure) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return filterFn(node2, nodeDict, exprResolveFns);
|
||||
|
|
@ -751,40 +789,18 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
if (obscurePass(row[`${attr}Id`], option)) {
|
||||
return true;
|
||||
}
|
||||
if (row[`${attr}Id`]) {
|
||||
const node2 = (0, lodash_1.get)(this.store, `${relation}.${row[`${attr}Id`]}`);
|
||||
if (!node2) {
|
||||
if (option?.obscure) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return filterFn(node2, nodeDict, exprResolveFns);
|
||||
}
|
||||
if (row[`${attr}Id`] === undefined) {
|
||||
// 说明一对多的外键没有取出来,需要抛出RowUnexists异常
|
||||
// 这个assert在count的情况下不满足 by Xc 20240205
|
||||
// assert(typeof projection[attr] === 'object');
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上的${attr}Id不能确定值,可能会影响判定结果`);
|
||||
}
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
if (option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss(entity, `${attr}Id`);
|
||||
}
|
||||
throw new Exception_1.OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
}
|
||||
(0, assert_1.assert)(row[`${attr}Id`] === null);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
|
@ -792,13 +808,6 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
// 一对多的子查询
|
||||
const [otmEntity, otmForeignKey] = relation;
|
||||
const predicate = filter[attr][Demand_1.SUB_QUERY_PREDICATE_KEYWORD] || 'in';
|
||||
if (option?.obscure) {
|
||||
// 如果是obscure,则返回的集合中有没有都不能否决“可能有”或“并不全部是”,所以可以直接返回true
|
||||
if (['in', 'not all'].includes(predicate)) {
|
||||
self.push(() => true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const fk = otmForeignKey || 'entityId';
|
||||
const otmProjection = {
|
||||
id: 1,
|
||||
|
|
@ -807,8 +816,8 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
/**
|
||||
* in代表外键连接后至少有一行数据
|
||||
* not in代表外键连接后一行也不能有
|
||||
* all代表反外键连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反外键连接条件的至少有一行
|
||||
* all代表反连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反连接条件的至少有一行
|
||||
*
|
||||
* 此时还没有确定父行,只有查询中明确带有id的查询可以先执行,否则不执行,暂先这个逻辑 by Xc 20230725
|
||||
*/
|
||||
|
|
@ -821,31 +830,31 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
/**
|
||||
* in代表外键连接后至少有一行数据
|
||||
* not in代表外键连接后一行也不能有
|
||||
* all代表反外键连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反外键连接条件的至少有一行
|
||||
* all代表反连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反连接条件的至少有一行
|
||||
*/
|
||||
const otmFilter = !otmForeignKey ? Object.assign({
|
||||
const otmOriginFilter = !otmForeignKey ? Object.assign({
|
||||
entity,
|
||||
}, filter[attr]) : (0, lodash_1.cloneDeep)(filter[attr]);
|
||||
if (['noobscuret in', 'in'].includes(predicate)) {
|
||||
Object.assign(otmFilter, {
|
||||
}, (0, lodash_1.cloneDeep)(filter[attr])) : (0, lodash_1.cloneDeep)(filter[attr]);
|
||||
const otmFilter = ['not in', 'in'].includes(predicate) ? (0, filter_1.combineFilters)(otmEntity, this.getSchema(), [
|
||||
otmOriginFilter, {
|
||||
[fk]: row.id,
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.assign(otmFilter, {
|
||||
[fk]: {
|
||||
$ne: row.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
const option2 = Object.assign({}, option, { nodeDict, dontCollect: true });
|
||||
}
|
||||
]) : {
|
||||
$not: otmOriginFilter,
|
||||
[fk]: row.id,
|
||||
};
|
||||
const subQuerySet = (this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: otmFilter,
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, context, option2)).map((ele) => {
|
||||
}, context, {
|
||||
...option,
|
||||
nodeDict,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
})).map((ele) => {
|
||||
return (ele)[fk];
|
||||
});
|
||||
switch (predicate) {
|
||||
|
|
@ -864,28 +873,28 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
});
|
||||
};
|
||||
if (filter.id && typeof filter.id === 'string') {
|
||||
const otmFilter = !otmForeignKey ? Object.assign({
|
||||
const otmOriginFilter = !otmForeignKey ? Object.assign({
|
||||
entity,
|
||||
}, filter[attr]) : (0, lodash_1.cloneDeep)(filter[attr]);
|
||||
if (['not in', 'in'].includes(predicate)) {
|
||||
Object.assign(otmFilter, {
|
||||
}, (0, lodash_1.cloneDeep)(filter[attr])) : (0, lodash_1.cloneDeep)(filter[attr]);
|
||||
const otmFilter = ['not in', 'in'].includes(predicate) ? (0, filter_1.combineFilters)(otmEntity, this.getSchema(), [
|
||||
otmOriginFilter, {
|
||||
[fk]: filter.id,
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.assign(otmFilter, {
|
||||
[fk]: {
|
||||
$ne: filter.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]) : {
|
||||
$not: otmOriginFilter,
|
||||
[fk]: filter.id,
|
||||
};
|
||||
try {
|
||||
const subQuerySet = (this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: otmFilter,
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, context, { dontCollect: true })).map((ele) => {
|
||||
}, context, {
|
||||
...option,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
})).map((ele) => {
|
||||
return (ele)[fk];
|
||||
});
|
||||
self.push((node) => {
|
||||
|
|
@ -921,12 +930,15 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
/**
|
||||
* 尝试用hashjoin将内表数组取出,因为memory中的表都不会太大,且用不了索引(未来优化了可能可以用id直接取值),因而用hash应当会更快
|
||||
*/
|
||||
const option2 = Object.assign({}, option, { dontCollect: true });
|
||||
try {
|
||||
const subQueryRows = this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: filter[attr],
|
||||
}, context, option2);
|
||||
}, context, {
|
||||
...option,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
});
|
||||
const buckets = (0, lodash_1.groupBy)(subQueryRows, fk);
|
||||
otm.push((node, nodeDict) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
|
|
@ -1357,7 +1369,6 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
}
|
||||
// 先计算projection,formResult只处理abjoint的行,不需要考虑expression和一对多多对一关系
|
||||
let rows2 = [];
|
||||
const incompletedRowIds = [];
|
||||
const { data: projection } = selection;
|
||||
for (const row of rows) {
|
||||
const result = {};
|
||||
|
|
@ -1365,7 +1376,9 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
const rel = this.judgeRelation(entity, attr);
|
||||
if (rel === 1) {
|
||||
if (row[attr] === undefined) {
|
||||
incompletedRowIds.push(row.id);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上的属性${attr}缺失,可能会影响上层使用结果,请确定`);
|
||||
}
|
||||
// break;
|
||||
}
|
||||
else if (typeof projection[attr] === 'number') {
|
||||
|
|
@ -1428,29 +1441,6 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
}
|
||||
rows2.push(result);
|
||||
}
|
||||
if (incompletedRowIds.length > 0) {
|
||||
// 如果有缺失属性的行,则报OakRowUnexistedException错误
|
||||
// fixed: 这里不报了。按约定框架应当保证取到要访问的属性
|
||||
// fixed: 和外键缺失一样,还是报,上层在知道框架会保证取到的情况下用allowMiss忽略此错误
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity}上有属性缺失,可能会影响上层使用结果,请确定`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Exception_1.OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: {
|
||||
$in: incompletedRowIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
}
|
||||
}
|
||||
// 再计算sorter
|
||||
if (sorter) {
|
||||
const sorterFn = this.translateSorter(entity, sorter, context, option);
|
||||
|
|
@ -1495,7 +1485,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
else {
|
||||
(0, assert_1.assert)([0, 1].includes(rel));
|
||||
result2[k] = row2[k];
|
||||
(0, assert_1.assert)(['string', 'number', 'boolean'].includes(typeof row2[k]));
|
||||
(0, assert_1.assert)(['string', 'number', 'boolean'].includes(typeof row2[k]) || row2[k] === null);
|
||||
key += `${row2[k]}`;
|
||||
values.push(row2[k]);
|
||||
}
|
||||
|
|
@ -1593,8 +1583,9 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
result[op] = 0;
|
||||
const data = distinct ? (0, lodash_1.uniq)(results[op]) : results[op];
|
||||
data.forEach((ele) => {
|
||||
(0, assert_1.assert)(typeof ele === 'number', '只有number类型的属性才可以计算sum');
|
||||
result[op] += ele;
|
||||
if (typeof ele === 'number') {
|
||||
result[op] += ele;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (op.startsWith('#count')) {
|
||||
|
|
@ -1609,11 +1600,14 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
else if (op.startsWith('#avg')) {
|
||||
result[op] = 0;
|
||||
const data = (distinct ? (0, lodash_1.uniq)(results[op]) : results[op]).filter(ele => ![undefined, null].includes(ele));
|
||||
let count = 0;
|
||||
data.forEach((ele) => {
|
||||
(0, assert_1.assert)(typeof ele === 'number', '只有number类型的属性才可以计算avg');
|
||||
result[op] += ele;
|
||||
if (typeof ele === 'number') {
|
||||
result[op] += ele;
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
result[op] = result[op] / data.length;
|
||||
result[op] = result[op] / count;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -1686,6 +1680,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
indexFrom,
|
||||
count,
|
||||
};
|
||||
(0, CascadeStore_1.polishSelection)(this.getSchema(), entity, selection);
|
||||
const result = await this.cascadeSelectAsync(entity, selection, context, Object.assign({}, option, {
|
||||
dontCollect: true,
|
||||
}));
|
||||
|
|
@ -1884,7 +1879,7 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
if (d instanceof Array) {
|
||||
for (const dd of d) {
|
||||
(0, assert_1.assert)(dd[Entity_1.UpdateAtAttribute], `获取的${e}对象数据没有update时间戳`);
|
||||
if (!this.store[e][dd.id]) {
|
||||
if (!this.store[e]?.[dd.id]) {
|
||||
// 有可能后台通过socket等途径先把数据推到了前台,这里直接忽略就行了
|
||||
this.updateAbjointRow(e, {
|
||||
id: 'dummy',
|
||||
|
|
@ -1932,16 +1927,19 @@ class TreeStore extends CascadeStore_1.CascadeStore {
|
|||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'create');
|
||||
}
|
||||
else if (this.store[entity][id].$current?.[Entity_1.UpdateAtAttribute] <= d[entity][id][Entity_1.UpdateAtAttribute]) {
|
||||
this.updateAbjointRow(entity, {
|
||||
id: 'dummy',
|
||||
action: 'update',
|
||||
data: d[entity][id],
|
||||
filter: {
|
||||
id,
|
||||
},
|
||||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'update');
|
||||
else if (this.store[entity]?.[id]) {
|
||||
const row = this.constructRow(this.store[entity][id], context);
|
||||
if (row[Entity_1.UpdateAtAttribute] <= d[entity][id][Entity_1.UpdateAtAttribute]) {
|
||||
this.updateAbjointRow(entity, {
|
||||
id: 'dummy',
|
||||
action: 'update',
|
||||
data: d[entity][id],
|
||||
filter: {
|
||||
id,
|
||||
},
|
||||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'update');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "oak-memory-tree-store",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.15",
|
||||
"description": "oak框架中内存级store的实现",
|
||||
"author": {
|
||||
"name": "XuChang"
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
"es/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"oak-domain": "^5.1.1",
|
||||
"oak-domain": "file:../oak-domain",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
374
src/store.ts
374
src/store.ts
|
|
@ -9,16 +9,17 @@ import {
|
|||
EntityDict, SelectOption, DeleteAtAttribute, AggregationResult, AggregationOp, CreateAtAttribute, UpdateAtAttribute, UpdateOperation
|
||||
} from "oak-domain/lib/types/Entity";
|
||||
import { ExpressionKey, EXPRESSION_PREFIX, NodeId, RefAttr, SUB_QUERY_PREDICATE_KEYWORD, SubQueryPredicateMetadata } from 'oak-domain/lib/types/Demand';
|
||||
import { OakCongruentRowExists, OakException, OakRowUnexistedException } from 'oak-domain/lib/types/Exception';
|
||||
import { OakCongruentRowExists, OakException } from 'oak-domain/lib/types/Exception';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||||
import { StorageSchema } from 'oak-domain/lib/types/Storage';
|
||||
import { ExprResolveFn, NodeDict, RowNode } from "./types/type";
|
||||
import { isRefAttrNode, Q_BooleanValue, Q_FullTextValue, Q_NumberValue, Q_StringValue } from 'oak-domain/lib/types/Demand';
|
||||
import { judgeRelation } from 'oak-domain/lib/store/relation';
|
||||
// Expression引入,下方声明改为any防止报错
|
||||
import { execOp, Expression, ExpressionConstant, isExpression, opMultipleParams } from 'oak-domain/lib/types/Expression';
|
||||
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
||||
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
||||
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
||||
import { CascadeStore, polishSelection } from 'oak-domain/lib/store/CascadeStore';
|
||||
import { Context, CreateOpResult, SeqAttribute } from 'oak-domain/lib/types';
|
||||
import { combineFilters, getRelevantIds } from 'oak-domain/lib/store/filter';
|
||||
|
||||
|
|
@ -31,9 +32,6 @@ interface ExprNodeTranslator {
|
|||
(row: any, nodeDict: NodeDict): ExpressionConstant | ExprLaterCheckFn;
|
||||
};
|
||||
|
||||
function obscurePass(value: any, option?: SelectOption): boolean {
|
||||
return !!(option?.obscure && value === undefined);
|
||||
}
|
||||
|
||||
class OakExpressionUnresolvedException<ED extends EntityDict & BaseEntityDict> extends OakException<ED> {
|
||||
|
||||
|
|
@ -42,6 +40,11 @@ class OakExpressionUnresolvedException<ED extends EntityDict & BaseEntityDict> e
|
|||
export interface TreeStoreSelectOption extends SelectOption {
|
||||
nodeDict?: NodeDict;
|
||||
disableSubQueryHashjoin?: boolean; // 禁用hashjoin优化,可能会导致性能下降严重(对比测试用)
|
||||
warnWhenAttributeMiss?: boolean; // 当有属性miss时,报一个Warning(debug使用)
|
||||
}
|
||||
|
||||
function showWarningAttributeMiss<ED extends EntityDict & BaseEntityDict>(entity: keyof ED, attr: string) {
|
||||
console.warn(`attribute miss: entity: ${entity as string}, attr: ${attr}`);
|
||||
}
|
||||
|
||||
export interface TreeStoreOperateOption extends OperateOption {
|
||||
|
|
@ -134,8 +137,12 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
}) {
|
||||
this.store = {};
|
||||
const now = Date.now();
|
||||
const schema = this.getSchema();
|
||||
for (const entity in data) {
|
||||
const { attributes } = this.getSchema()[entity];
|
||||
if (!schema[entity]) {
|
||||
throw new Error(`reset unknown entity:[${entity as string}]`);
|
||||
}
|
||||
const { attributes } = schema[entity];
|
||||
this.store[entity] = {};
|
||||
for (const row of data[entity]!) {
|
||||
for (const key in attributes) {
|
||||
|
|
@ -170,7 +177,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
});
|
||||
}
|
||||
else {
|
||||
this.setMaxSeq(entity, row[SeqAttribute]);
|
||||
this.setMaxSeq(entity, row[SeqAttribute] + 1);
|
||||
}
|
||||
assert(row.id && !row.id.includes('.'));
|
||||
set(this.store, `${entity}.${row.id}.$current`, row);
|
||||
|
|
@ -213,12 +220,12 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
}
|
||||
|
||||
private constructRow<Cxt extends Context, OP extends TreeStoreSelectOption>(node: RowNode, context: Cxt, option?: OP) {
|
||||
let data = cloneDeep(node.$current);
|
||||
if (context.getCurrentTxnId() && node.$txnId === context.getCurrentTxnId()) {
|
||||
if (!node.$next) {
|
||||
// 如果要求返回delete数据,返回带$$deleteAt$$的行
|
||||
if (option?.includedDeleted) {
|
||||
return Object.assign({}, data, {
|
||||
// bug fixed,这里如果是自己create再删除,data也是null
|
||||
if (node.$current && option?.includedDeleted) {
|
||||
return Object.assign({}, node.$current, {
|
||||
[DeleteAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
|
|
@ -226,19 +233,21 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
}
|
||||
else if (!node.$current) {
|
||||
// 本事务创建的,若在cache中$$createAt$$和$$updateAt$$置为1
|
||||
return Object.assign({}, data, node.$next, context instanceof SyncContext && {
|
||||
return Object.assign({}, node.$current, node.$next, context instanceof SyncContext && {
|
||||
[CreateAtAttribute]: 1,
|
||||
[UpdateAtAttribute]: 1,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// 本事务更新的,若在cache中$$updateAt$$置为1
|
||||
return Object.assign({}, data, node.$next, context instanceof SyncContext && {
|
||||
return Object.assign({}, node.$current, node.$next, context instanceof SyncContext && {
|
||||
[UpdateAtAttribute]: 1,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return {
|
||||
...node.$current,
|
||||
};
|
||||
}
|
||||
|
||||
private testFilterFns(
|
||||
|
|
@ -390,7 +399,8 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
*/
|
||||
private translateExpressionNode<T extends keyof ED, OP extends TreeStoreSelectOption, Cxt extends Context>(
|
||||
entity: T,
|
||||
expression: Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']> | ExpressionConstant,
|
||||
// expression: Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']> | ExpressionConstant,
|
||||
expression: any,
|
||||
context: Cxt,
|
||||
option?: OP): ExprNodeTranslator | ExpressionConstant {
|
||||
|
||||
|
|
@ -399,7 +409,8 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const option2 = (expression as any)[op];
|
||||
|
||||
if (opMultipleParams(op)) {
|
||||
const paramsTranslated = (option2 as (Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']>)[]).map(
|
||||
const paramsTranslated = (option2 as any[]).map(
|
||||
// const paramsTranslated = (option2 as (Expression<keyof ED[T]['Schema']> | RefAttr<keyof ED[T]['Schema']>)[]).map(
|
||||
ele => this.translateExpressionNode(entity, ele, context, option2)
|
||||
);
|
||||
|
||||
|
|
@ -419,7 +430,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
);
|
||||
|
||||
if (!later) {
|
||||
return execOp(op, results, option2.obscure);
|
||||
return execOp(op, results);
|
||||
}
|
||||
const laterCheckFn = (nodeDict2: NodeDict) => {
|
||||
results = results.map(
|
||||
|
|
@ -434,7 +445,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
if (results.find(ele => typeof ele === 'function')) {
|
||||
return laterCheckFn;
|
||||
}
|
||||
return execOp(op, results, option && option.obscure);
|
||||
return execOp(op, results);
|
||||
};
|
||||
return laterCheckFn;
|
||||
}
|
||||
|
|
@ -454,12 +465,12 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
}
|
||||
return laterCheckFn;
|
||||
}
|
||||
return execOp(op, result, option2.obscure);
|
||||
return execOp(op, result);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return () => {
|
||||
return execOp(op, paramsTranslated, option2.obscure);
|
||||
return execOp(op, paramsTranslated);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -469,9 +480,15 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
return (row, nodeDict) => {
|
||||
if (expression.hasOwnProperty('#attr')) {
|
||||
// 说明是本结点的属性;
|
||||
return row[(expression as {
|
||||
const attr = row[(expression as {
|
||||
'#attr': keyof ED[T]['Schema'];
|
||||
})['#attr']] as ExpressionConstant;
|
||||
if (attr === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss<ED>(entity, (expression as {
|
||||
'#attr': string;
|
||||
})['#attr']);
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
else {
|
||||
assert(expression.hasOwnProperty('#refId'));
|
||||
|
|
@ -501,7 +518,8 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
|
||||
private translateExpression<T extends keyof ED, OP extends TreeStoreSelectOption, Cxt extends Context>(
|
||||
entity: T,
|
||||
expression: Expression<keyof ED[T]['Schema']>,
|
||||
// expression: Expression<keyof ED[T]['Schema']>,
|
||||
expression: any,
|
||||
context: Cxt, option?: OP): (row: Partial<ED[T]['OpSchema']>, nodeDict: NodeDict) => ExpressionConstant | ExprLaterCheckFn {
|
||||
const expr = this.translateExpressionNode(entity, expression, context, option);
|
||||
|
||||
|
|
@ -530,92 +548,110 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option) as any;
|
||||
for (const attr of attributes) {
|
||||
const { name } = attr;
|
||||
if (row && row[name] && (typeof row[name] === 'string' && row[name].includes($search) || obscurePass(row[name], option))) {
|
||||
return true;
|
||||
if (row) {
|
||||
for (const attr of attributes) {
|
||||
const { name } = attr;
|
||||
const value = row[name];
|
||||
if (value === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss<ED>(entity, name as string);
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && row[name].includes($search)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private translatePredicate<OP extends TreeStoreSelectOption>(path: string, predicate: string, value: any, option?: OP): (row: Record<string, any>) => boolean {
|
||||
private translatePredicate<OP extends TreeStoreSelectOption>(entity: keyof ED, path: string, predicate: string, value: any, option?: OP): (row: Record<string, any>) => boolean {
|
||||
switch (predicate) {
|
||||
case '$gt': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data > value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data > value;
|
||||
};
|
||||
}
|
||||
case '$lt': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data < value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data < value;
|
||||
};
|
||||
} case '$gte': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data >= value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data >= value;
|
||||
};
|
||||
}
|
||||
case '$lte': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data <= value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data <= value;
|
||||
};
|
||||
}
|
||||
case '$eq': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data === value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data === value;
|
||||
};
|
||||
}
|
||||
case '$ne': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data !== value || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data !== value;
|
||||
};
|
||||
}
|
||||
case '$between': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['number', 'string'].includes(typeof data) && data >= value[0] && data <= value[1] || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['number', 'string'].includes(typeof data) && data >= value[0] && data <= value[1];
|
||||
};
|
||||
}
|
||||
case '$mod': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return typeof data === 'number' && data % value[0] === value[1] || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return typeof data === 'number' && data % value[0] === value[1];
|
||||
};
|
||||
}
|
||||
case '$startsWith': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.startsWith(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['string'].includes(typeof data) && data.startsWith(value);
|
||||
};
|
||||
}
|
||||
case '$endsWith': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.endsWith(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['string'].includes(typeof data) && data.endsWith(value);
|
||||
};
|
||||
}
|
||||
case '$includes': {
|
||||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
return ['string'].includes(typeof data) && data.includes(value) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return ['string'].includes(typeof data) && data.includes(value);
|
||||
};
|
||||
}
|
||||
case '$exists': {
|
||||
|
|
@ -623,11 +659,12 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
return (row) => {
|
||||
// JsonFilter有可能是根结点,path为空
|
||||
const data = path ? get(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
if (value) {
|
||||
return ![null, undefined].includes(data) || obscurePass(data, option);
|
||||
return ![null, undefined].includes(data);
|
||||
}
|
||||
else {
|
||||
return [null, undefined].includes(data) || obscurePass(data, option);
|
||||
return [null, undefined].includes(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -635,14 +672,16 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
assert(value instanceof Array);
|
||||
return (row) => {
|
||||
const data = get(row, path);
|
||||
return value.includes(data) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return value.includes(data);
|
||||
};
|
||||
}
|
||||
case '$nin': {
|
||||
assert(value instanceof Array);
|
||||
return (row) => {
|
||||
const data = get(row, path);
|
||||
return !value.includes(data) || obscurePass(data, option);
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return !value.includes(data);
|
||||
};
|
||||
}
|
||||
case '$contains': {
|
||||
|
|
@ -650,12 +689,13 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const array = value instanceof Array ? value : [value];
|
||||
return (row) => {
|
||||
const data = path ? get(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return differenceBy(array, data, (value: any) => {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}).length === 0 || obscurePass(data, option);
|
||||
}).length === 0;
|
||||
};
|
||||
}
|
||||
case '$overlaps': {
|
||||
|
|
@ -663,21 +703,37 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const array = value instanceof Array ? value : [value];
|
||||
return (row) => {
|
||||
const data = path ? get(row, path) : row;
|
||||
data === undefined && option?.warnWhenAttributeMiss && showWarningAttributeMiss<ED>(entity, path);
|
||||
return intersectionBy(array, data, (value: any) => {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}).length > 0 || obscurePass(data, option);
|
||||
}).length > 0;
|
||||
};
|
||||
}
|
||||
case '$length': {
|
||||
// json中的数组长度查询
|
||||
const length = value;
|
||||
return (row) => {
|
||||
const data = path ? get(row, path) : row;
|
||||
assert(data instanceof Array, '$length operator can only used on array attribute');
|
||||
if (typeof length === 'number') {
|
||||
return data.length === length;
|
||||
}
|
||||
else {
|
||||
const op = Object.keys(length)[0];
|
||||
return this.translatePredicate(entity, 'length', op, length[op], option)(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(`predicate ${predicate} is not recoganized`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private translateObjectPredicate(filter: Record<string, any>) {
|
||||
private translateObjectPredicate(entity: keyof ED, filter: Record<string, any>) {
|
||||
const fns: Array<(value: any) => boolean> = [];
|
||||
const translatePredicateInner = (p: Record<string, any> | Array<any>, path: string, fns2: Array<(value: any) => boolean>) => {
|
||||
if (p instanceof Array) {
|
||||
|
|
@ -686,7 +742,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const path2 = `${path}[${idx}]`;
|
||||
if (typeof ele !== 'object') {
|
||||
if (![null, undefined].includes(ele)) {
|
||||
fns2.push(this.translatePredicate(path2, '$eq', ele));
|
||||
fns2.push(this.translatePredicate(entity, path2, '$eq', ele));
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -721,14 +777,14 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
else if (attr.startsWith('$')) {
|
||||
assert(Object.keys(p).length === 1);
|
||||
fns2.push(
|
||||
this.translatePredicate(path, attr, p[attr])
|
||||
this.translatePredicate(entity, path, attr, p[attr])
|
||||
);
|
||||
}
|
||||
else {
|
||||
const attr2 = attr.startsWith('.') ? attr.slice(1) : attr;
|
||||
const path2 = path ? `${path}.${attr2}` : attr2;
|
||||
if (typeof p[attr] !== 'object') {
|
||||
fns2.push(this.translatePredicate(path2, '$eq', p[attr]));
|
||||
fns2.push(this.translatePredicate(entity, path2, '$eq', p[attr]));
|
||||
}
|
||||
else {
|
||||
translatePredicateInner(p[attr], path2, fns2);
|
||||
|
|
@ -756,15 +812,18 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
attr: string,
|
||||
context: Cxt,
|
||||
option?: OP): (node: RowNode, nodeDict: NodeDict, exprResolveFns: Array<ExprResolveFn>) => boolean {
|
||||
// 如果是模糊查询且该属性为undefined,说明没取到,返回true
|
||||
function obscurePassLocal(row: any) {
|
||||
return obscurePass(row[attr], option);
|
||||
}
|
||||
if (!['object', 'array'].includes(this.getSchema()[entity].attributes[attr]?.type)) {
|
||||
if (typeof filter !== 'object') {
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
return row ? (row as any)[attr] === filter || obscurePassLocal(row) : false;
|
||||
if (row) {
|
||||
const value = (row as any)[attr];
|
||||
if (value === undefined && option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss<ED>(entity, attr);
|
||||
}
|
||||
return value === filter;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
else {
|
||||
|
|
@ -774,7 +833,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
throw new Error('子查询已经改用一对多的外键连接方式');
|
||||
}
|
||||
else {
|
||||
const fn = this.translatePredicate(attr, predicate, (filter as Record<string, any>)[predicate], option);
|
||||
const fn = this.translatePredicate(entity, attr, predicate, (filter as Record<string, any>)[predicate], option);
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
if (!row) {
|
||||
|
|
@ -799,13 +858,13 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
}
|
||||
}
|
||||
else {
|
||||
const fn = this.translateObjectPredicate(filter);
|
||||
const fn = this.translateObjectPredicate(entity, filter);
|
||||
return (node) => {
|
||||
const row = this.constructRow(node, context, option);
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return fn((row as any)[attr]) || obscurePassLocal(row);
|
||||
return fn((row as any)[attr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -876,27 +935,11 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
if (obscurePass((row as any).entity, option) || obscurePass((row as any).entityId, option)) {
|
||||
return true;
|
||||
}
|
||||
if ((row as any).entityId === undefined || (row as any).entity === undefined) {
|
||||
// 这个assert在count的情况下不满足 by Xc 20240205
|
||||
// assert(typeof projection[attr] === 'object');
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity as string}上的entity/entityId不能确定值,可能会影响判定结果`);
|
||||
}
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
if (option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss<ED>(entity, 'entity/entityId');
|
||||
}
|
||||
throw new OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
}
|
||||
if ((row as any).entity !== attr) {
|
||||
return false;
|
||||
|
|
@ -906,9 +949,6 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
}
|
||||
const node2 = get(this.store, `${attr}.${(row as any).entityId}`);
|
||||
if (!node2) {
|
||||
if (option?.obscure) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return filterFn(node2, nodeDict, exprResolveFns);
|
||||
|
|
@ -924,40 +964,18 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
if (obscurePass((row as any)[`${attr}Id`], option)) {
|
||||
return true;
|
||||
}
|
||||
if ((row as any)[`${attr}Id`]) {
|
||||
const node2 = get(this.store, `${relation}.${(row as any)[`${attr}Id`]}`);
|
||||
if (!node2) {
|
||||
if (option?.obscure) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return filterFn(node2, nodeDict, exprResolveFns);
|
||||
}
|
||||
if ((row as any)[`${attr}Id`] === undefined) {
|
||||
// 说明一对多的外键没有取出来,需要抛出RowUnexists异常
|
||||
// 这个assert在count的情况下不满足 by Xc 20240205
|
||||
// assert(typeof projection[attr] === 'object');
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity as string}上的${attr}Id不能确定值,可能会影响判定结果`);
|
||||
}
|
||||
return false; // 若不能确定,认定为条件不满足
|
||||
if (option?.warnWhenAttributeMiss) {
|
||||
showWarningAttributeMiss<ED>(entity, `${attr}Id`);
|
||||
}
|
||||
throw new OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
}
|
||||
assert((row as any)[`${attr}Id`] === null);
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
|
@ -967,13 +985,6 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const [otmEntity, otmForeignKey] = relation;
|
||||
const predicate: NonNullable<SubQueryPredicateMetadata['#sqp']> = filter[attr][SUB_QUERY_PREDICATE_KEYWORD] || 'in';
|
||||
|
||||
if (option?.obscure) {
|
||||
// 如果是obscure,则返回的集合中有没有都不能否决“可能有”或“并不全部是”,所以可以直接返回true
|
||||
if (['in', 'not all'].includes(predicate)) {
|
||||
self.push(() => true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const fk = otmForeignKey || 'entityId';
|
||||
const otmProjection = {
|
||||
id: 1,
|
||||
|
|
@ -982,8 +993,8 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
/**
|
||||
* in代表外键连接后至少有一行数据
|
||||
* not in代表外键连接后一行也不能有
|
||||
* all代表反外键连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反外键连接条件的至少有一行
|
||||
* all代表反连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反连接条件的至少有一行
|
||||
*
|
||||
* 此时还没有确定父行,只有查询中明确带有id的查询可以先执行,否则不执行,暂先这个逻辑 by Xc 20230725
|
||||
*/
|
||||
|
|
@ -996,31 +1007,32 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
/**
|
||||
* in代表外键连接后至少有一行数据
|
||||
* not in代表外键连接后一行也不能有
|
||||
* all代表反外键连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反外键连接条件的至少有一行
|
||||
* all代表反连接条件的一行也不能有(符合的是否至少要有一行?直觉上没这个限制)
|
||||
* not all 代表反连接条件的至少有一行
|
||||
*/
|
||||
const otmFilter = !otmForeignKey ? Object.assign({
|
||||
const otmOriginFilter = !otmForeignKey ? Object.assign({
|
||||
entity,
|
||||
}, filter[attr]) : cloneDeep(filter[attr]);
|
||||
if (['noobscuret in', 'in'].includes(predicate)) {
|
||||
Object.assign(otmFilter, {
|
||||
}, cloneDeep(filter[attr])) : cloneDeep(filter[attr]);
|
||||
|
||||
const otmFilter = ['not in', 'in'].includes(predicate) ? combineFilters(otmEntity, this.getSchema(), [
|
||||
otmOriginFilter, {
|
||||
[fk]: row.id,
|
||||
});
|
||||
}
|
||||
]) : {
|
||||
$not: otmOriginFilter,
|
||||
[fk]: row.id,
|
||||
}
|
||||
else {
|
||||
Object.assign(otmFilter, {
|
||||
[fk]: {
|
||||
$ne: row.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
const option2 = Object.assign({}, option, { nodeDict, dontCollect: true });
|
||||
const subQuerySet = (this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: otmFilter,
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, context, option2)).map(
|
||||
}, context, {
|
||||
...option,
|
||||
nodeDict,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
})).map(
|
||||
(ele) => {
|
||||
return (ele)[fk] as string | null;
|
||||
}
|
||||
|
|
@ -1041,21 +1053,18 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
});
|
||||
};
|
||||
|
||||
if (filter.id && typeof filter.id === 'string') {
|
||||
const otmFilter = !otmForeignKey ? Object.assign({
|
||||
if (filter.id && typeof filter.id === 'string') {
|
||||
const otmOriginFilter = !otmForeignKey ? Object.assign({
|
||||
entity,
|
||||
}, filter[attr]) : cloneDeep(filter[attr]);
|
||||
if (['not in', 'in'].includes(predicate)) {
|
||||
Object.assign(otmFilter, {
|
||||
}, cloneDeep(filter[attr])) : cloneDeep(filter[attr]);
|
||||
|
||||
const otmFilter = ['not in', 'in'].includes(predicate) ? combineFilters(otmEntity, this.getSchema(), [
|
||||
otmOriginFilter, {
|
||||
[fk]: filter.id,
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.assign(otmFilter, {
|
||||
[fk]: {
|
||||
$ne: filter.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
]) : {
|
||||
$not: otmOriginFilter,
|
||||
[fk]: filter.id,
|
||||
}
|
||||
try {
|
||||
const subQuerySet = (this.selectAbjointRow(otmEntity, {
|
||||
|
|
@ -1063,7 +1072,11 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
filter: otmFilter,
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
}, context, { dontCollect: true })).map(
|
||||
}, context, {
|
||||
...option,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
})).map(
|
||||
(ele) => {
|
||||
return (ele)[fk] as string | null;
|
||||
}
|
||||
|
|
@ -1102,12 +1115,15 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
/**
|
||||
* 尝试用hashjoin将内表数组取出,因为memory中的表都不会太大,且用不了索引(未来优化了可能可以用id直接取值),因而用hash应当会更快
|
||||
*/
|
||||
const option2 = Object.assign({}, option, { dontCollect: true });
|
||||
try {
|
||||
const subQueryRows = this.selectAbjointRow(otmEntity, {
|
||||
data: otmProjection,
|
||||
filter: filter[attr],
|
||||
}, context, option2);
|
||||
}, context, {
|
||||
...option,
|
||||
dontCollect: true,
|
||||
warnWhenAttributeMiss: false, // 一对多连接不必要考虑这个属性缺失
|
||||
});
|
||||
|
||||
const buckets = groupBy(subQueryRows, fk);
|
||||
|
||||
|
|
@ -1620,7 +1636,6 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
|
||||
// 先计算projection,formResult只处理abjoint的行,不需要考虑expression和一对多多对一关系
|
||||
let rows2: Array<Partial<ED[T]['Schema']>> = [];
|
||||
const incompletedRowIds: string[] = [];
|
||||
const { data: projection } = selection;
|
||||
for (const row of rows) {
|
||||
const result = {} as Partial<ED[T]['Schema']>;
|
||||
|
|
@ -1628,7 +1643,9 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const rel = this.judgeRelation(entity, attr);
|
||||
if (rel === 1) {
|
||||
if (row[attr] === undefined) {
|
||||
incompletedRowIds.push(row.id!);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity as string}上的属性${attr}缺失,可能会影响上层使用结果,请确定`);
|
||||
}
|
||||
// break;
|
||||
}
|
||||
else if (typeof projection[attr] === 'number') {
|
||||
|
|
@ -1695,29 +1712,6 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
}
|
||||
rows2.push(result);
|
||||
}
|
||||
if (incompletedRowIds.length > 0) {
|
||||
// 如果有缺失属性的行,则报OakRowUnexistedException错误
|
||||
// fixed: 这里不报了。按约定框架应当保证取到要访问的属性
|
||||
// fixed: 和外键缺失一样,还是报,上层在知道框架会保证取到的情况下用allowMiss忽略此错误
|
||||
if (option?.ignoreAttrMiss) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`对象${entity as string}上有属性缺失,可能会影响上层使用结果,请确定`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new OakRowUnexistedException([{
|
||||
entity,
|
||||
selection: {
|
||||
data: projection,
|
||||
filter: {
|
||||
id: {
|
||||
$in: incompletedRowIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
// 再计算sorter
|
||||
if (sorter) {
|
||||
|
|
@ -1775,7 +1769,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
else {
|
||||
assert([0, 1].includes(rel as number));
|
||||
(result2 as any)[k] = row2[k];
|
||||
assert(['string', 'number', 'boolean'].includes(typeof row2[k]));
|
||||
assert(['string', 'number', 'boolean'].includes(typeof row2[k]) || row2[k] === null);
|
||||
key += `${row2[k]}`;
|
||||
values.push(row2[k]);
|
||||
}
|
||||
|
|
@ -1886,8 +1880,9 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const data = distinct ? uniq(results[op]) : results[op];
|
||||
data.forEach(
|
||||
(ele) => {
|
||||
assert(typeof ele === 'number', '只有number类型的属性才可以计算sum');
|
||||
result[op] += ele;
|
||||
if (typeof ele === 'number') {
|
||||
result[op] += ele;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1907,13 +1902,16 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
const data = (distinct ? uniq(results[op]) : results[op]).filter(
|
||||
ele => ![undefined, null].includes(ele)
|
||||
);
|
||||
let count = 0;
|
||||
data.forEach(
|
||||
(ele) => {
|
||||
assert(typeof ele === 'number', '只有number类型的属性才可以计算avg');
|
||||
result[op] += ele;
|
||||
if (typeof ele === 'number') {
|
||||
result[op] += ele;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
);
|
||||
result[op] = result[op]/data.length;
|
||||
result[op] = result[op]/count;
|
||||
}
|
||||
}
|
||||
return result as AggregationResult<ED[T]['Schema']>[number];
|
||||
|
|
@ -2021,6 +2019,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
indexFrom,
|
||||
count,
|
||||
};
|
||||
polishSelection(this.getSchema(), entity, selection);
|
||||
|
||||
const result = await this.cascadeSelectAsync(entity, selection, context, Object.assign({}, option, {
|
||||
dontCollect: true,
|
||||
|
|
@ -2252,7 +2251,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
if (d instanceof Array) {
|
||||
for (const dd of d) {
|
||||
assert(dd[UpdateAtAttribute], `获取的${e as string}对象数据没有update时间戳`);
|
||||
if(!this.store[e]![dd.id]) {
|
||||
if(!this.store[e]?.[dd.id]) {
|
||||
// 有可能后台通过socket等途径先把数据推到了前台,这里直接忽略就行了
|
||||
this.updateAbjointRow(e, {
|
||||
id: 'dummy',
|
||||
|
|
@ -2300,16 +2299,19 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
} as ED[keyof ED]['CreateSingle'], context, option2);
|
||||
this.addToOperationResult(result, entity, 'create');
|
||||
}
|
||||
else if (this.store[entity]![id].$current?.[UpdateAtAttribute] <= d[entity]![id]![UpdateAtAttribute]) {
|
||||
this.updateAbjointRow(entity, {
|
||||
id: 'dummy',
|
||||
action: 'update',
|
||||
data: d[entity]![id] as any,
|
||||
filter: {
|
||||
id,
|
||||
} as any,
|
||||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'update');
|
||||
else if (this.store[entity]?.[id]) {
|
||||
const row = this.constructRow(this.store[entity]![id]!, context);
|
||||
if ((row![UpdateAtAttribute] as number) <= (d[entity]![id]![UpdateAtAttribute] as number)) {
|
||||
this.updateAbjointRow(entity, {
|
||||
id: 'dummy',
|
||||
action: 'update',
|
||||
data: d[entity]![id] as any,
|
||||
filter: {
|
||||
id,
|
||||
} as any,
|
||||
}, context, option2);
|
||||
this.addToOperationResult(result, entity, 'update');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2324,7 +2326,7 @@ export default class TreeStore<ED extends EntityDict & BaseEntityDict> extends C
|
|||
id: 'dummy',
|
||||
action: 'update',
|
||||
data: d,
|
||||
filter: combineFilters(e, this.getSchema(), [f, {
|
||||
filter: combineFilters(e, this.getSchema(), [f!, {
|
||||
[UpdateAtAttribute]: {
|
||||
$lte: d[UpdateAtAttribute],
|
||||
}
|
||||
|
|
|
|||
54
test/test.ts
54
test/test.ts
|
|
@ -24,6 +24,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-2',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -39,6 +40,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -201,6 +203,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -216,6 +219,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -267,6 +271,7 @@ describe('基础测试', function () {
|
|||
entity: 'user-id-1',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -282,6 +287,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -358,6 +364,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -373,6 +380,7 @@ describe('基础测试', function () {
|
|||
entity: 'user3',
|
||||
entityId: 'user3-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -440,6 +448,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -455,6 +464,7 @@ describe('基础测试', function () {
|
|||
entity: 'user3',
|
||||
entityId: 'user3-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -518,6 +528,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -533,6 +544,7 @@ describe('基础测试', function () {
|
|||
entity: 'user3',
|
||||
entityId: 'user3-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -621,6 +633,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -686,6 +699,7 @@ describe('基础测试', function () {
|
|||
entity: 'user',
|
||||
entityId: 'user-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -701,6 +715,7 @@ describe('基础测试', function () {
|
|||
entity: 'user3',
|
||||
entityId: 'user3-id-1',
|
||||
modi: {
|
||||
id: generateNewId(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: generateNewId(),
|
||||
|
|
@ -1302,6 +1317,43 @@ describe('基础测试', function () {
|
|||
}
|
||||
}, context, {});
|
||||
|
||||
const row12 = store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
data: {
|
||||
price: {
|
||||
$length: 3,
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row13 = store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
data: {
|
||||
price: {
|
||||
$length: {
|
||||
$gt: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
context.commit();
|
||||
assert(row.length === 1);
|
||||
assert(row2.length === 0);
|
||||
|
|
@ -1314,6 +1366,8 @@ describe('基础测试', function () {
|
|||
assert(row9.length === 1);
|
||||
assert(row10.length === 1);
|
||||
assert(row11.length === 0);
|
||||
assert(row12.length === 1);
|
||||
assert(row13.length === 0);
|
||||
// console.log(JSON.stringify(row7));
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue