parent
e9059b8514
commit
5858dd7db0
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ class MySqlConnector {
|
|||
}
|
||||
async exec(sql, txn) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// console.log(sql);
|
||||
// console.log(`${sql}; \n`);
|
||||
}
|
||||
if (txn) {
|
||||
const connection = this.txnDict[txn];
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class PostgreSQLConnector {
|
|||
}
|
||||
async exec(sql, txn) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// console.log('SQL:', sql);
|
||||
// console.log(`SQL: ${sql}; \n`);
|
||||
}
|
||||
try {
|
||||
let result;
|
||||
|
|
|
|||
|
|
@ -499,7 +499,8 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
|
|||
}
|
||||
else if (attr2 === '$contains') {
|
||||
// PostgreSQL 使用 @> 操作符检查 JSON 包含关系
|
||||
const value = JSON.stringify(o[attr2]);
|
||||
const targetArr = Array.isArray(o[attr2]) ? o[attr2] : [o[attr2]];
|
||||
const value = JSON.stringify(targetArr);
|
||||
if (stmt2) {
|
||||
stmt2 += ' AND ';
|
||||
}
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -10,19 +10,15 @@
|
|||
"lib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"test": "node --stack-size=65500 ./node_modules/mocha/bin/mocha -g",
|
||||
"make:test:domain": "ts-node script/makeTestDomain.ts",
|
||||
"build": "tsc",
|
||||
"build:test": "tsc -p tsconfig.test.json",
|
||||
"test:mysql": "node ./test-dist/testMySQLStore.js",
|
||||
"test:postgres": "node ./test-dist/testPostgresStore.js"
|
||||
"build": "node --stack-size=4096 ./script/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"oak-domain": "file:../oak-domain",
|
||||
"oak-general-business": "file:../oak-general-business",
|
||||
"pg": "^8.16.3",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
|
@ -32,13 +28,13 @@
|
|||
"@types/luxon": "^2.3.2",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/node": "^20.6.0",
|
||||
"@types/pg": "^8.15.6",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"mocha": "^10.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^5.9"
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
const ts = require('typescript');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { cwd } = require('process');
|
||||
|
||||
// 解析命令行参数
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let configPath = 'tsconfig.json';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '-p' || args[i] === '--project') {
|
||||
if (i + 1 < args.length) {
|
||||
configPath = args[i + 1];
|
||||
break;
|
||||
} else {
|
||||
console.error('error: option \'-p, --project\' argument missing');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configPath;
|
||||
}
|
||||
|
||||
// ANSI 颜色代码
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
cyan: '\x1b[36m',
|
||||
red: '\x1b[91m',
|
||||
yellow: '\x1b[93m',
|
||||
gray: '\x1b[90m'
|
||||
};
|
||||
|
||||
function compile(configPath) {
|
||||
// 读取 tsconfig.json
|
||||
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
||||
|
||||
if (configFile.error) {
|
||||
console.error(ts.formatDiagnostic(configFile.error, {
|
||||
getCanonicalFileName: f => f,
|
||||
getCurrentDirectory: process.cwd,
|
||||
getNewLine: () => '\n'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
const parsedConfig = ts.parseJsonConfigFileContent(
|
||||
configFile.config,
|
||||
ts.sys,
|
||||
path.dirname(configPath)
|
||||
);
|
||||
|
||||
if (parsedConfig.errors.length > 0) {
|
||||
parsedConfig.errors.forEach(diagnostic => {
|
||||
console.error(ts.formatDiagnostic(diagnostic, {
|
||||
getCanonicalFileName: f => f,
|
||||
getCurrentDirectory: process.cwd,
|
||||
getNewLine: () => '\n'
|
||||
}));
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 创建编译程序
|
||||
// 根据配置决定是否使用增量编译
|
||||
let program;
|
||||
if (parsedConfig.options.incremental || parsedConfig.options.composite) {
|
||||
// 对于增量编译,使用 createIncrementalProgram
|
||||
const host = ts.createIncrementalCompilerHost(parsedConfig.options);
|
||||
program = ts.createIncrementalProgram({
|
||||
rootNames: parsedConfig.fileNames,
|
||||
options: parsedConfig.options,
|
||||
host: host,
|
||||
configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfig),
|
||||
});
|
||||
} else {
|
||||
// 普通编译
|
||||
program = ts.createProgram({
|
||||
rootNames: parsedConfig.fileNames,
|
||||
options: parsedConfig.options,
|
||||
});
|
||||
}
|
||||
|
||||
// 执行编译
|
||||
const emitResult = program.emit();
|
||||
|
||||
// 获取诊断信息
|
||||
const allDiagnostics = ts
|
||||
.getPreEmitDiagnostics(program)
|
||||
.concat(emitResult.diagnostics);
|
||||
|
||||
// 输出诊断信息
|
||||
allDiagnostics.forEach(diagnostic => {
|
||||
if (diagnostic.file) {
|
||||
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
|
||||
diagnostic.start
|
||||
);
|
||||
const message = ts.flattenDiagnosticMessageText(
|
||||
diagnostic.messageText,
|
||||
'\n'
|
||||
);
|
||||
const isError = diagnostic.category === ts.DiagnosticCategory.Error;
|
||||
const category = isError ? 'error' : 'warning';
|
||||
const categoryColor = isError ? colors.red : colors.yellow;
|
||||
|
||||
console.log(
|
||||
`${colors.cyan}${diagnostic.file.fileName}${colors.reset}:${colors.yellow}${line + 1}${colors.reset}:${colors.yellow}${character + 1}${colors.reset} - ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${message}`
|
||||
);
|
||||
} else {
|
||||
const isError = diagnostic.category === ts.DiagnosticCategory.Error;
|
||||
const category = isError ? 'error' : 'warning';
|
||||
const categoryColor = isError ? colors.red : colors.yellow;
|
||||
|
||||
console.log(
|
||||
`${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 输出编译统计
|
||||
const errorCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Error).length;
|
||||
const warningCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Warning).length;
|
||||
|
||||
if (errorCount > 0 || warningCount > 0) {
|
||||
if (allDiagnostics.length > 0) {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (errorCount > 0) {
|
||||
parts.push(`${errorCount} error${errorCount !== 1 ? 's' : ''}`);
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
parts.push(`${warningCount} warning${warningCount !== 1 ? 's' : ''}`);
|
||||
}
|
||||
console.log(`Found ${parts.join(' and ')}.`);
|
||||
}
|
||||
|
||||
// tsc 的行为:
|
||||
// 1. 默认情况下(noEmitOnError: false):
|
||||
// - 即使有类型错误也会生成 .js 文件
|
||||
// - 但如果有错误,退出码是 1
|
||||
// 2. noEmitOnError: true 时:
|
||||
// - 有错误时不生成文件(emitSkipped 为 true)
|
||||
// - 退出码是 1
|
||||
// 3. 没有错误时:
|
||||
// - 生成文件,退出码 0
|
||||
|
||||
// 无论 emitSkipped 与否,只要有错误就应该退出 1
|
||||
if (errorCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Compilation completed successfully.');
|
||||
}
|
||||
|
||||
// 执行编译
|
||||
const configPathArg = parseArgs();
|
||||
let configPath;
|
||||
|
||||
// 判断参数是目录还是文件
|
||||
if (fs.existsSync(configPathArg)) {
|
||||
const stat = fs.statSync(configPathArg);
|
||||
if (stat.isDirectory()) {
|
||||
// 如果是目录,拼接 tsconfig.json
|
||||
configPath = path.resolve(configPathArg, 'tsconfig.json');
|
||||
} else {
|
||||
// 如果是文件,直接使用
|
||||
configPath = path.resolve(configPathArg);
|
||||
}
|
||||
} else {
|
||||
// 尝试作为相对路径解析
|
||||
configPath = path.resolve(cwd(), configPathArg);
|
||||
if (!fs.existsSync(configPath)) {
|
||||
// 如果还是不存在,尝试添加 tsconfig.json
|
||||
const dirPath = path.resolve(cwd(), configPathArg);
|
||||
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
||||
configPath = path.join(dirPath, 'tsconfig.json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(`error TS5058: The specified path does not exist: '${configPath}'.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
compile(configPath);
|
||||
|
|
@ -4,6 +4,6 @@ import {
|
|||
} from 'oak-domain/lib/compiler/schemalBuilder';
|
||||
|
||||
analyzeEntities(`${process.cwd()}/node_modules/oak-domain/src/entities`, 'oak-domain/lib/entities')
|
||||
analyzeEntities(`${process.cwd()}/node_modules/oak-general-business/src/entities`, 'oak-general-business/lib/entities');
|
||||
// analyzeEntities(`${process.cwd()}/node_modules/oak-general-business/src/entities`, 'oak-general-business/lib/entities');
|
||||
analyzeEntities(`${process.cwd()}/test/entities`);
|
||||
buildSchema(`${process.cwd()}/test/test-app-domain`);
|
||||
|
|
@ -53,8 +53,9 @@ export class MySqlConnector {
|
|||
|
||||
async exec(sql: string, txn?: string) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// console.log(sql);
|
||||
// console.log(`${sql}; \n`);
|
||||
}
|
||||
|
||||
if (txn) {
|
||||
const connection = this.txnDict[txn];
|
||||
assert(connection);
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export class PostgreSQLConnector {
|
|||
|
||||
async exec(sql: string, txn?: string): Promise<[QueryResultRow[], QueryResult]> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// console.log('SQL:', sql);
|
||||
// console.log(`SQL: ${sql}; \n`);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -573,7 +573,8 @@ export class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extend
|
|||
}
|
||||
} else if (attr2 === '$contains') {
|
||||
// PostgreSQL 使用 @> 操作符检查 JSON 包含关系
|
||||
const value = JSON.stringify(o[attr2]);
|
||||
const targetArr = Array.isArray(o[attr2]) ? o[attr2] : [o[attr2]];
|
||||
const value = JSON.stringify(targetArr);
|
||||
if (stmt2) {
|
||||
stmt2 += ' AND ';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { String, Int, Datetime, Image, Boolean, Text } from 'oak-domain/lib/types/DataType';
|
||||
import { EntityShape } from 'oak-domain/lib/types/Entity';
|
||||
import { Schema as System } from './System';
|
||||
|
||||
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
|
||||
|
||||
export type AppType = 'web' | 'wechatMp' | 'wechatPublic' | 'native';
|
||||
|
||||
export interface Schema extends EntityShape {
|
||||
name: String<32>;
|
||||
description?: Text;
|
||||
type: AppType;
|
||||
system: System;
|
||||
};
|
||||
|
||||
const entityDesc: EntityDesc<
|
||||
Schema,
|
||||
'',
|
||||
'',
|
||||
{
|
||||
type: Schema['type'];
|
||||
}
|
||||
> = {
|
||||
locales: {
|
||||
zh_CN: {
|
||||
name: '应用',
|
||||
attr: {
|
||||
description: '描述',
|
||||
type: '类型',
|
||||
system: '系统',
|
||||
name: '名称',
|
||||
},
|
||||
v: {
|
||||
type: {
|
||||
web: '网站',
|
||||
wechatPublic: '微信公众号',
|
||||
wechatMp: '微信小程序',
|
||||
native: 'App',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
style: {
|
||||
color: {
|
||||
type: {
|
||||
wechatMp: '#32CD32',
|
||||
web: '#00FF7F',
|
||||
wechatPublic: '#90EE90',
|
||||
native: '#008000',
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { String, Geo } from 'oak-domain/lib/types/DataType';
|
||||
import { EntityShape, Configuration } from 'oak-domain/lib/types/Entity';
|
||||
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
|
||||
|
||||
export interface Schema extends EntityShape {
|
||||
name: String<32>;
|
||||
level: 'province' | 'city' | 'district' | 'street' | 'country';
|
||||
depth: 0 | 1 | 2 | 3 | 4;
|
||||
parent?: Schema;
|
||||
code: String<12>;
|
||||
center: Geo;
|
||||
};
|
||||
|
||||
|
||||
const entityDesc: EntityDesc<Schema, '', '', {
|
||||
level: Schema['level'];
|
||||
}> = {
|
||||
locales: {
|
||||
zh_CN: {
|
||||
name: '地区',
|
||||
attr: {
|
||||
level: '层级',
|
||||
depth: '深度',
|
||||
parent: '上级地区',
|
||||
name: '名称',
|
||||
code: '地区编码',
|
||||
center: '中心坐标',
|
||||
},
|
||||
v: {
|
||||
level: {
|
||||
country: '国家',
|
||||
province: '省',
|
||||
city: '市',
|
||||
district: '区',
|
||||
street: '街道',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
configuration: {
|
||||
actionType: 'readOnly',
|
||||
static: true,
|
||||
},
|
||||
style: {
|
||||
color: {
|
||||
level: {
|
||||
province: '#00FF7F',
|
||||
city: '#1E90FF',
|
||||
district: '#4682B4',
|
||||
street: '#808080',
|
||||
country: '#2F4F4F',
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,27 +1,27 @@
|
|||
import { String, Int, Datetime, Image, Boolean, Text, Float } from 'oak-domain/lib/types/DataType';
|
||||
import { Schema as Area } from 'oak-general-business/lib/entities/Area';
|
||||
import { Schema as User } from 'oak-general-business/lib/entities/User';
|
||||
import { Schema as ExtraFile } from 'oak-general-business/lib/entities/ExtraFile';
|
||||
import { EntityShape } from 'oak-domain/lib/types';
|
||||
import { LocaleDef } from 'oak-domain/lib/types/Locale';
|
||||
import { String, Int, Datetime, Image, Boolean, Text, Float } from 'oak-domain/lib/types/DataType';
|
||||
import { Schema as User } from './User';
|
||||
import { Schema as Area } from './Area';
|
||||
import { EntityDesc, EntityShape } from 'oak-domain/lib/types';
|
||||
|
||||
export interface Schema extends EntityShape {
|
||||
district: String<16>;
|
||||
area: Area;
|
||||
owner: User;
|
||||
dd: Array<ExtraFile>;
|
||||
size: Float<8, 4>;
|
||||
data?: Object;
|
||||
};
|
||||
|
||||
const locale: LocaleDef<Schema, '', '', {}> = {
|
||||
zh_CN: {
|
||||
name: '房屋',
|
||||
attr: {
|
||||
district: '街区',
|
||||
area: '地区',
|
||||
owner: '房主',
|
||||
dd: '文件',
|
||||
size: '面积',
|
||||
const entityDesc: EntityDesc<Schema> = {
|
||||
locales: {
|
||||
zh_CN: {
|
||||
name: '房屋',
|
||||
attr: {
|
||||
district: '街区',
|
||||
area: '地区',
|
||||
owner: '房主',
|
||||
size: '面积',
|
||||
data: '数据',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { String, Int, Datetime, Image, Boolean, Text } from 'oak-domain/lib/types/DataType';
|
||||
import { EntityShape } from 'oak-domain/lib/types/Entity';
|
||||
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
|
||||
|
||||
export interface Schema extends EntityShape {
|
||||
name: String<32>;
|
||||
description?: Text;
|
||||
entity?: String<32>; // System是抽象对象,应用上级与之一对一的对象可以使用双向指针,以方便编程
|
||||
entityId?: String<64>;
|
||||
};
|
||||
|
||||
export const entityDesc: EntityDesc<Schema> = {
|
||||
locales: {
|
||||
zh_CN: {
|
||||
name: '平台',
|
||||
attr: {
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
entity: '关联对象',
|
||||
entityId: '关联对象id',
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { String, Boolean, Text } from 'oak-domain/lib/types/DataType';
|
||||
import { EntityShape } from 'oak-domain/lib/types/Entity';
|
||||
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
|
||||
import { Schema as Platform } from './Platform';
|
||||
|
||||
export interface Schema extends EntityShape {
|
||||
name: String<32>;
|
||||
description?: Text;
|
||||
folder?: String<16>;
|
||||
platform?: Platform;
|
||||
super?: Boolean; // super表示是这个platform本身的系统,可以操作application/system这些数据,也可以访问超出本system的其它数据。
|
||||
entity?: String<32>; // System是抽象对象,应用上级与之一对一的对象可以使用双向指针,以方便编程
|
||||
entityId?: String<64>;
|
||||
};
|
||||
|
||||
export const entityDesc: EntityDesc<Schema> = {
|
||||
locales: {
|
||||
zh_CN: {
|
||||
name: '系统',
|
||||
attr: {
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
super: '超级系统',
|
||||
folder: '代码目录名',
|
||||
entity: '关联对象',
|
||||
entityId: '关联对象id',
|
||||
platform: '平台',
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { String, Int, Datetime, Image, Boolean } from 'oak-domain/lib/types/DataType';
|
||||
import { Schema as User } from 'oak-domain/lib/entities/User';
|
||||
import { Schema as Application } from './Application';
|
||||
import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action';
|
||||
import { EntityShape } from 'oak-domain/lib/types/Entity';
|
||||
import { ActionDef } from 'oak-domain/lib/types/Action';
|
||||
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
|
||||
import { Environment } from 'oak-domain/lib/types/Environment';
|
||||
|
||||
export interface Schema extends EntityShape {
|
||||
application?: Application;
|
||||
entity?: String<32>;
|
||||
entityId?: String<64>;
|
||||
user?: User;
|
||||
player?: User;
|
||||
disablesAt?: Datetime;
|
||||
env: Environment;
|
||||
refreshedAt: Datetime;
|
||||
value: String<64>;
|
||||
oldValue?: String<64>;
|
||||
};
|
||||
|
||||
export type Action = AbleAction;
|
||||
|
||||
export const AbleActionDef: ActionDef<AbleAction, AbleState> = makeAbleActionDef('enabled');
|
||||
|
||||
export const entityDesc: EntityDesc<
|
||||
Schema,
|
||||
Action,
|
||||
'',
|
||||
{
|
||||
ableState: AbleState;
|
||||
}
|
||||
> = {
|
||||
locales: {
|
||||
zh_CN: {
|
||||
name: '令牌',
|
||||
attr: {
|
||||
application: '应用',
|
||||
entity: '关联对象',
|
||||
entityId: '关联对象id',
|
||||
user: '用户',
|
||||
player: '扮演者',
|
||||
env: '环境',
|
||||
ableState: '状态',
|
||||
disablesAt: '禁用时间',
|
||||
refreshedAt: '刷新时间',
|
||||
value: '令牌值',
|
||||
oldValue: "老令牌",
|
||||
},
|
||||
action: {
|
||||
enable: '激活',
|
||||
disable: '禁用',
|
||||
},
|
||||
v: {
|
||||
ableState: {
|
||||
enabled: '使用中',
|
||||
disabled: '已禁用',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
indexes: [
|
||||
{
|
||||
name: 'index_value',
|
||||
attributes: [
|
||||
{
|
||||
name: 'value',
|
||||
},
|
||||
{
|
||||
name: '$$deleteAt$$',
|
||||
},
|
||||
],
|
||||
config: {
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
style: {
|
||||
icon: {
|
||||
enable: '',
|
||||
disable: '',
|
||||
},
|
||||
color: {
|
||||
ableState: {
|
||||
enabled: '#008000',
|
||||
disabled: '#A9A9A9',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
import { String, Int, Text, Boolean, Datetime } from 'oak-domain/lib/types/DataType';
|
||||
import { ActionDef } from 'oak-domain/lib/types/Action';
|
||||
import { EntityShape } from 'oak-domain/lib/types/Entity';
|
||||
import { EntityDesc } from 'oak-domain/lib/types/EntityDesc';
|
||||
import { Schema as User } from 'oak-domain/lib/entities/User';
|
||||
|
||||
export interface Schema extends User {
|
||||
passwordSha1?: Text;
|
||||
birth?: Datetime;
|
||||
gender?: 'male' | 'female';
|
||||
idCardType?: 'ID-Card' | 'passport' | 'Mainland-passport';
|
||||
idNumber?: String<32>;
|
||||
isRoot?: Boolean;
|
||||
hasPassword?: Boolean;
|
||||
};
|
||||
|
||||
export type IdAction = 'verify' | 'accept' | 'reject';
|
||||
export type IdState = 'unverified' | 'verified' | 'verifying';
|
||||
export const IdActionDef: ActionDef<IdAction, IdState> = {
|
||||
stm: {
|
||||
verify: ['unverified', 'verifying'],
|
||||
accept: [['unverified', 'verifying'], 'verified'],
|
||||
reject: [['verifying', 'verified'], 'unverified'],
|
||||
},
|
||||
is: 'unverified',
|
||||
};
|
||||
|
||||
export type UserAction = 'activate' | 'disable' | 'enable' | 'mergeTo' | 'mergeFrom';
|
||||
export type UserState = 'shadow' | 'normal' | 'disabled' | 'merged';
|
||||
export const UserActionDef: ActionDef<UserAction, UserState> = {
|
||||
stm: {
|
||||
activate: ['shadow', 'normal'],
|
||||
disable: [['normal', 'shadow'], 'disabled'],
|
||||
enable: ['disabled', 'normal'],
|
||||
mergeTo: [['normal', 'shadow'], 'merged'],
|
||||
mergeFrom: ['normal', 'normal'],
|
||||
},
|
||||
};
|
||||
|
||||
export type Action = UserAction | IdAction;
|
||||
|
||||
export const entityDesc: EntityDesc<
|
||||
Schema,
|
||||
Action,
|
||||
'',
|
||||
{
|
||||
userState: UserState;
|
||||
idState: IdState;
|
||||
gender: Required<Schema>['gender'];
|
||||
idCardType: Required<Schema>['idCardType'];
|
||||
}
|
||||
> = {
|
||||
locales: {
|
||||
zh_CN: {
|
||||
name: '用户',
|
||||
attr: {
|
||||
name: '姓名',
|
||||
nickname: '昵称',
|
||||
birth: '生日',
|
||||
password: '密码',
|
||||
passwordSha1: 'sha1加密密码',
|
||||
gender: '性别',
|
||||
idCardType: '证件类型',
|
||||
idNumber: '证件号码',
|
||||
ref: '指向用户',
|
||||
userState: '用户状态',
|
||||
idState: '认证状态',
|
||||
isRoot: '是否超级用户',
|
||||
hasPassword: '用户是否存在密码'
|
||||
},
|
||||
action: {
|
||||
activate: '激活',
|
||||
accept: '同意',
|
||||
verify: '认证',
|
||||
reject: '拒绝',
|
||||
enable: '启用',
|
||||
disable: '禁用',
|
||||
mergeTo: '合并',
|
||||
mergeFrom: '使合并',
|
||||
},
|
||||
v: {
|
||||
userState: {
|
||||
shadow: '未激活',
|
||||
normal: '正常',
|
||||
disabled: '禁用',
|
||||
merged: '已被合并',
|
||||
},
|
||||
idState: {
|
||||
unverified: '未认证',
|
||||
verifying: '认证中',
|
||||
verified: '已认证',
|
||||
},
|
||||
gender: {
|
||||
male: '男',
|
||||
female: '女',
|
||||
},
|
||||
idCardType: {
|
||||
'ID-Card': '身份证',
|
||||
passport: '护照',
|
||||
'Mainland-passport': '港澳台通行证',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
indexes: [
|
||||
{
|
||||
name: 'index_birth',
|
||||
attributes: [
|
||||
{
|
||||
name: 'birth',
|
||||
direction: 'ASC',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'index_fulltext',
|
||||
attributes: [
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
name: 'nickname',
|
||||
},
|
||||
],
|
||||
config: {
|
||||
type: 'fulltext',
|
||||
parser: 'ngram',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'index_userState_refId',
|
||||
attributes: [
|
||||
{
|
||||
name: 'userState',
|
||||
},
|
||||
{
|
||||
name: 'ref',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
style: {
|
||||
icon: {
|
||||
verify: '',
|
||||
accept: '',
|
||||
reject: '',
|
||||
activate: '',
|
||||
enable: '',
|
||||
disable: '',
|
||||
mergeTo: '',
|
||||
mergeFrom: '',
|
||||
},
|
||||
color: {
|
||||
userState: {
|
||||
normal: '#0000FF',
|
||||
disabled: '#FF0000',
|
||||
merged: '#9A9A9A',
|
||||
shadow: '#D3D3D3',
|
||||
},
|
||||
idState: {
|
||||
unverified: '#FF0000',
|
||||
verified: '#0000FF',
|
||||
verifying: '#EEE8AA',
|
||||
},
|
||||
gender: {
|
||||
male: '#0000FF',
|
||||
female: '#EE82EE',
|
||||
},
|
||||
idCardType: {
|
||||
'ID-Card': '#E0FFFF',
|
||||
'Mainland-passport': '#2E8B57',
|
||||
passport: '#2F4F4F',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
import assert from 'assert';
|
||||
import { TestContext } from './Context';
|
||||
import { v4 } from 'uuid';
|
||||
import { MysqlStore } from '../lib/MySQL/store';
|
||||
import { MysqlStore } from '../src/MySQL/store';
|
||||
import { EntityDict, storageSchema } from './test-app-domain';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { WebConfig } from './test-app-domain/Application/Schema';
|
||||
import { describe, it, before, after } from './utils/test';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { tests } from './testcase';
|
||||
|
||||
describe('test mysqlstore', async function () {
|
||||
this.timeout(60000);
|
||||
let store: MysqlStore<EntityDict, TestContext>;
|
||||
|
||||
before(async () => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { PostgreSQLStore } from '../lib/PostgreSQL/store';
|
||||
import { PostgreSQLStore } from '../src/PostgreSQL/store';
|
||||
import assert from 'assert';
|
||||
import { TestContext } from './Context';
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityDict, storageSchema } from './test-app-domain';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { WebConfig } from './test-app-domain/Application/Schema';
|
||||
import { describe, it, before, after } from './utils/test';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { tests } from './testcase';
|
||||
|
||||
describe('test postgresstore', async function () {
|
||||
this.timeout(60000);
|
||||
let store: PostgreSQLStore<EntityDict, TestContext>;
|
||||
|
||||
before(async () => {
|
||||
|
|
|
|||
|
|
@ -33,10 +33,6 @@ describe('test MysqlTranslator', function () {
|
|||
id: 1,
|
||||
$$createAt$$: 1,
|
||||
userId: 1,
|
||||
mobile: {
|
||||
id: 1,
|
||||
mobile: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
// console.log(sql);
|
||||
|
|
@ -47,10 +43,6 @@ describe('test MysqlTranslator', function () {
|
|||
id: 1,
|
||||
$$createAt$$: 1,
|
||||
userId: 1,
|
||||
mobile: {
|
||||
id: 1,
|
||||
mobile: 1,
|
||||
},
|
||||
},
|
||||
distinct: true,
|
||||
});
|
||||
|
|
@ -110,10 +102,9 @@ describe('test MysqlTranslator', function () {
|
|||
},
|
||||
},
|
||||
'#aggr': {
|
||||
email: {
|
||||
email: 1,
|
||||
$$createAt$$: 1,
|
||||
},
|
||||
env: {
|
||||
type: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
|
|
@ -158,24 +149,10 @@ describe('test MysqlTranslator', function () {
|
|||
id: 1,
|
||||
$$createAt$$: 1,
|
||||
userId: 1,
|
||||
mobile: {
|
||||
id: 1,
|
||||
mobile: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id: 'xc',
|
||||
$$createAt$$: 1,
|
||||
mobile: {
|
||||
$or: [
|
||||
{
|
||||
id: 'mob',
|
||||
},
|
||||
{
|
||||
mobile: '135',
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(sql);
|
||||
|
|
@ -190,14 +167,15 @@ describe('test MysqlTranslator', function () {
|
|||
filter: {
|
||||
id: 'xc',
|
||||
$$createAt$$: 1,
|
||||
mobile$user: {
|
||||
id: '123',
|
||||
},
|
||||
$and: [
|
||||
{
|
||||
mobile$user: {
|
||||
"#sqp": 'all',
|
||||
mobile: '456',
|
||||
token$user: {
|
||||
entity: 'email',
|
||||
}
|
||||
},
|
||||
{
|
||||
token$user: {
|
||||
entity: 'mobile',
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,323 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 聚合函数测试 ====================
|
||||
|
||||
it('[7.1]aggregation $$count', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const systemId = v4();
|
||||
await store.operate('system', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: systemId,
|
||||
name: 'test-count',
|
||||
description: 'test',
|
||||
folder: '/test',
|
||||
application$system: [
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1', description: 't1', type: 'web' } },
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app2', description: 't2', type: 'web' } },
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app3', description: 't3', type: 'wechatMp' } },
|
||||
]
|
||||
}
|
||||
} as EntityDict['system']['CreateSingle'], context, {});
|
||||
await context.commit();
|
||||
|
||||
const result = await store.aggregate('application', {
|
||||
data: {
|
||||
'#aggr': { systemId: 1 },
|
||||
'#count-1': { id: 1 }
|
||||
},
|
||||
filter: { systemId }
|
||||
}, context, {});
|
||||
|
||||
assert(result.length === 1, `Expected 1 group`);
|
||||
assert(result[0]['#count-1'] === 3, `Expected count 3, got ${result[0]['#count-1']}`);
|
||||
});
|
||||
|
||||
it('[1.17]aggregation functions $$sum, $$max, $$min, $$avg', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const areaId = v4();
|
||||
const ownerId = v4();
|
||||
|
||||
// 创建多个房屋记录用于测试聚合函数
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: v4(),
|
||||
areaId,
|
||||
ownerId,
|
||||
district: '杭州',
|
||||
size: 80.0,
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
areaId,
|
||||
ownerId,
|
||||
district: '杭州',
|
||||
size: 100.0,
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
areaId,
|
||||
ownerId,
|
||||
district: '杭州',
|
||||
size: 120.0,
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
areaId,
|
||||
ownerId,
|
||||
district: '上海',
|
||||
size: 150.0,
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
|
||||
// 测试 $$count
|
||||
const countResult = await store.aggregate('house', {
|
||||
data: {
|
||||
'#aggr': {
|
||||
district: 1,
|
||||
},
|
||||
'#count-1': {
|
||||
id: 1,
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
areaId,
|
||||
},
|
||||
}, context, {});
|
||||
|
||||
assert(countResult.length === 2, `Expected 2 groups, got ${countResult.length}`);
|
||||
const hangzhouCount = countResult.find(r => r['#data']?.district === '杭州');
|
||||
const shanghaiCount = countResult.find(r => r['#data']?.district === '上海');
|
||||
assert(hangzhouCount && hangzhouCount['#count-1'] === 3, `Expected 3 houses in 杭州, got ${hangzhouCount?.['#count-1']}`);
|
||||
assert(shanghaiCount && shanghaiCount['#count-1'] === 1, `Expected 1 house in 上海, got ${shanghaiCount?.['#count-1']}`);
|
||||
|
||||
// TODO: 下面的测试用例暂不支持,先注释掉
|
||||
// 测试 $$sum
|
||||
// const sumResult = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': {
|
||||
// district: 1,
|
||||
// },
|
||||
// '#sum-1': {
|
||||
// $$sum: {
|
||||
// '#attr': 'size',
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
// filter: {
|
||||
// areaId,
|
||||
// },
|
||||
// }, context, {});
|
||||
|
||||
// const hangzhouSum = sumResult.find(r => r['#data']?.district === '杭州');
|
||||
// assert(hangzhouSum && hangzhouSum['#sum-1'] === 300, `Expected sum 300 for 杭州, got ${hangzhouSum?.['#sum-1']}`);
|
||||
|
||||
// 测试 $$max
|
||||
// const maxResult = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': {
|
||||
// district: 1,
|
||||
// },
|
||||
// '#max-1': {
|
||||
// $$max: {
|
||||
// '#attr': 'size',
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
// filter: {
|
||||
// areaId,
|
||||
// },
|
||||
// }, context, {});
|
||||
|
||||
// const hangzhouMax = maxResult.find(r => r['#data']?.district === '杭州');
|
||||
// assert(hangzhouMax && hangzhouMax['#max-1'] === 120, `Expected max 120 for 杭州, got ${hangzhouMax?.['#max-1']}`);
|
||||
|
||||
// 测试 $$min
|
||||
// const minResult = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': {
|
||||
// district: 1,
|
||||
// },
|
||||
// '#min-1': {
|
||||
// $$min: {
|
||||
// '#attr': 'size',
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
// filter: {
|
||||
// areaId,
|
||||
// },
|
||||
// }, context, {});
|
||||
|
||||
// const hangzhouMin = minResult.find(r => r['#data']?.district === '杭州');
|
||||
// assert(hangzhouMin && hangzhouMin['#min-1'] === 80, `Expected min 80 for 杭州, got ${hangzhouMin?.['#min-1']}`);
|
||||
|
||||
// 测试 $$avg
|
||||
// const avgResult = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': {
|
||||
// district: 1,
|
||||
// },
|
||||
// '#avg-1': {
|
||||
// $$avg: {
|
||||
// '#attr': 'size',
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
// filter: {
|
||||
// areaId,
|
||||
// },
|
||||
// }, context, {});
|
||||
|
||||
// const hangzhouAvg = avgResult.find(r => r['#data']?.district === '杭州');
|
||||
// assert(hangzhouAvg && hangzhouAvg['#avg-1'] === 100, `Expected avg 100 for 杭州, got ${hangzhouAvg?.['#avg-1']}`);
|
||||
});
|
||||
|
||||
// // TODO: 下面聚合暂不支持
|
||||
// it('[7.2]aggregation $$sum', async () => {
|
||||
// const context = new TestContext(store);
|
||||
// await context.begin();
|
||||
|
||||
// const areaId = v4();
|
||||
// await store.operate('house', {
|
||||
// id: v4(),
|
||||
// action: 'create',
|
||||
// data: [
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 100.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 150.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 200.0 },
|
||||
// ]
|
||||
// }, context, {});
|
||||
// await context.commit();
|
||||
|
||||
// const result = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': { areaId: 1 },
|
||||
// '#sum-1': { $$sum: { '#attr': 'size' } }
|
||||
// },
|
||||
// filter: { areaId }
|
||||
// }, context, {});
|
||||
|
||||
// assert(result.length === 1, `Expected 1 group`);
|
||||
// assert(result[0]['#sum-1'] === 450, `Expected sum 450, got ${result[0]['#sum-1']}`);
|
||||
// });
|
||||
|
||||
// it('[7.3]aggregation $$max $$min', async () => {
|
||||
// const context = new TestContext(store);
|
||||
// await context.begin();
|
||||
|
||||
// const areaId = v4();
|
||||
// await store.operate('house', {
|
||||
// id: v4(),
|
||||
// action: 'create',
|
||||
// data: [
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 80.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 120.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 200.0 },
|
||||
// ]
|
||||
// }, context, {});
|
||||
// await context.commit();
|
||||
|
||||
// const result = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': { areaId: 1 },
|
||||
// '#max-1': { $$max: { '#attr': 'size' } },
|
||||
// '#min-1': { $$min: { '#attr': 'size' } }
|
||||
// },
|
||||
// filter: { areaId }
|
||||
// }, context, {});
|
||||
|
||||
// assert(result.length === 1, `Expected 1 group`);
|
||||
// assert(result[0]['#max-1'] === 200, `Expected max 200, got ${result[0]['#max-1']}`);
|
||||
// assert(result[0]['#min-1'] === 80, `Expected min 80, got ${result[0]['#min-1']}`);
|
||||
// });
|
||||
|
||||
// it('[7.4]aggregation $$avg', async () => {
|
||||
// const context = new TestContext(store);
|
||||
// await context.begin();
|
||||
|
||||
// const areaId = v4();
|
||||
// await store.operate('house', {
|
||||
// id: v4(),
|
||||
// action: 'create',
|
||||
// data: [
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 100.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 200.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 300.0 },
|
||||
// ]
|
||||
// }, context, {});
|
||||
// await context.commit();
|
||||
|
||||
// const result = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': { areaId: 1 },
|
||||
// '#avg-1': { $$avg: { '#attr': 'size' } }
|
||||
// },
|
||||
// filter: { areaId }
|
||||
// }, context, {});
|
||||
|
||||
// assert(result.length === 1, `Expected 1 group`);
|
||||
// assert(result[0]['#avg-1'] === 200, `Expected avg 200, got ${result[0]['#avg-1']}`);
|
||||
// });
|
||||
|
||||
// it('[7.5]aggregation with multiple groups', async () => {
|
||||
// const context = new TestContext(store);
|
||||
// await context.begin();
|
||||
|
||||
// const areaId = v4();
|
||||
// await store.operate('house', {
|
||||
// id: v4(),
|
||||
// action: 'create',
|
||||
// data: [
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 100.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '杭州', size: 150.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '上海', size: 200.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '上海', size: 300.0 },
|
||||
// { id: v4(), areaId, ownerId: v4(), district: '北京', size: 250.0 },
|
||||
// ]
|
||||
// }, context, {});
|
||||
// await context.commit();
|
||||
|
||||
// const result = await store.aggregate('house', {
|
||||
// data: {
|
||||
// '#aggr': { district: 1 },
|
||||
// '#count-1': { id: 1 },
|
||||
// '#sum-1': { $$sum: { '#attr': 'size' } },
|
||||
// '#avg-1': { $$avg: { '#attr': 'size' } }
|
||||
// },
|
||||
// filter: { areaId }
|
||||
// }, context, {});
|
||||
|
||||
// assert(result.length === 3, `Expected 3 groups, got ${result.length}`);
|
||||
|
||||
// const hz = result.find(r => r['#data']?.district === '杭州');
|
||||
// const sh = result.find(r => r['#data']?.district === '上海');
|
||||
// const bj = result.find(r => r['#data']?.district === '北京');
|
||||
|
||||
// assert(hz && hz['#count-1'] === 2 && hz['#sum-1'] === 250, `杭州 aggregation failed`);
|
||||
// assert(sh && sh['#count-1'] === 2 && sh['#sum-1'] === 500, `上海 aggregation failed`);
|
||||
// assert(bj && bj['#count-1'] === 1 && bj['#sum-1'] === 250, `北京 aggregation failed`);
|
||||
// });
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,119 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 布尔运算符测试 ====================
|
||||
|
||||
it('[5.1]boolean expression $true $false', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{ id: id1, areaId: v4(), ownerId: v4(), district: '杭州', size: 100.0 },
|
||||
{ id: id2, areaId: v4(), ownerId: v4(), district: '上海', size: 50.0 },
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// $true: 检查表达式为真
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$true: { $gt: [{ '#attr': 'size' }, 80] }
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id1, `$true failed`);
|
||||
|
||||
// $false: 检查表达式为假
|
||||
const r2 = await context.select('house', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$false: { $gt: [{ '#attr': 'size' }, 80] }
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1 && r2[0].id === id2, `$false failed`);
|
||||
});
|
||||
|
||||
it('[5.2]logic expression $and $or $not', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
const id3 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{ id: id1, areaId: v4(), ownerId: v4(), district: '杭州', size: 80.0 },
|
||||
{ id: id2, areaId: v4(), ownerId: v4(), district: '上海', size: 120.0 },
|
||||
{ id: id3, areaId: v4(), ownerId: v4(), district: '北京', size: 100.0 },
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
const ids = [id1, id2, id3];
|
||||
|
||||
// $and: size > 70 AND size < 110
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $gt: [{ '#attr': 'size' }, 70] },
|
||||
{ $lt: [{ '#attr': 'size' }, 110] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 2, `$and failed, expected 2, got ${r1.length}`);
|
||||
|
||||
// $or: size < 90 OR size > 110
|
||||
const r2 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: {
|
||||
$or: [
|
||||
{ $lt: [{ '#attr': 'size' }, 90] },
|
||||
{ $gt: [{ '#attr': 'size' }, 110] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 2, `$or failed, expected 2, got ${r2.length}`);
|
||||
|
||||
// $not: NOT (size = 100)
|
||||
const r3 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: {
|
||||
$not: { $eq: [{ '#attr': 'size' }, 100] }
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r3.length === 2, `$not failed, expected 2, got ${r3.length}`);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 比较运算符测试 ====================
|
||||
|
||||
it('[4.1]comparison expressions $gt $gte $lt $lte $ne', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
const id3 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{ id: id1, areaId: v4(), ownerId: v4(), district: '杭州', size: 50.0 },
|
||||
{ id: id2, areaId: v4(), ownerId: v4(), district: '上海', size: 100.0 },
|
||||
{ id: id3, areaId: v4(), ownerId: v4(), district: '北京', size: 150.0 },
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
const ids = [id1, id2, id3];
|
||||
|
||||
// $gt: size > 100
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: { $gt: [{ '#attr': 'size' }, 100] }
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id3, `$gt failed`);
|
||||
|
||||
// $gte: size >= 100
|
||||
const r2 = await context.select('house', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: { $gte: [{ '#attr': 'size' }, 100] }
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 2, `$gte failed, expected 2, got ${r2.length}`);
|
||||
|
||||
// $lt: size < 100
|
||||
const r3 = await context.select('house', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: { $lt: [{ '#attr': 'size' }, 100] }
|
||||
}
|
||||
}, {});
|
||||
assert(r3.length === 1 && r3[0].id === id1, `$lt failed`);
|
||||
|
||||
// $lte: size <= 100
|
||||
const r4 = await context.select('house', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: { $lte: [{ '#attr': 'size' }, 100] }
|
||||
}
|
||||
}, {});
|
||||
assert(r4.length === 2, `$lte failed, expected 2, got ${r4.length}`);
|
||||
|
||||
// $ne: size != 100
|
||||
const r5 = await context.select('house', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: ids },
|
||||
$expr: { $ne: [{ '#attr': 'size' }, 100] }
|
||||
}
|
||||
}, {});
|
||||
assert(r5.length === 2, `$ne failed, expected 2, got ${r5.length}`);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 复合表达式测试 ====================
|
||||
|
||||
it('[8.1]nested math expressions', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 100.0,
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试嵌套表达式: size * 2 + 50 / 5 = 210
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{
|
||||
$divide: [
|
||||
{
|
||||
$add: [
|
||||
{ $multiply: [{ '#attr': 'size' }, 2] },
|
||||
50
|
||||
]
|
||||
},
|
||||
5
|
||||
]
|
||||
},
|
||||
210
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Nested math expression failed`);
|
||||
});
|
||||
|
||||
it('[8.2]complex logic with date and math', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const now = Date.now();
|
||||
const twoDaysAgo = now - 2 * 24 * 3600 * 1000;
|
||||
const fiveDaysAgo = now - 5 * 24 * 3600 * 1000;
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: twoDaysAgo,
|
||||
value: v4(),
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: fiveDaysAgo,
|
||||
value: v4(),
|
||||
}
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 查询: 刷新时间在3天内 OR 刷新时间超过4天
|
||||
const r1 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$or: [
|
||||
{
|
||||
$lt: [
|
||||
{ $dateDiff: [now, { '#attr': 'refreshedAt' }, 'd'] },
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
$gt: [
|
||||
{ $dateDiff: [now, { '#attr': 'refreshedAt' }, 'd'] },
|
||||
4
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 2, `Complex logic failed, expected 2, got ${r1.length}`);
|
||||
|
||||
// 查询: 刷新时间在3天到4天之间
|
||||
const r2 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$and: [
|
||||
{
|
||||
$gte: [
|
||||
{ $dateDiff: [now, { '#attr': 'refreshedAt' }, 'd'] },
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
$lte: [
|
||||
{ $dateDiff: [now, { '#attr': 'refreshedAt' }, 'd'] },
|
||||
4
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 0, `Complex logic failed, expected 0, got ${r2.length}`);
|
||||
});
|
||||
|
||||
it('[8.3]expression with cross-entity reference', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const systemId = v4();
|
||||
const appId1 = v4();
|
||||
const appId2 = v4();
|
||||
|
||||
await store.operate('system', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: systemId,
|
||||
name: 'parent_system',
|
||||
description: 'test',
|
||||
folder: '/test',
|
||||
application$system: [
|
||||
{
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: appId1,
|
||||
name: 'parent_system_app1',
|
||||
description: 't1',
|
||||
type: 'web',
|
||||
}
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: appId2,
|
||||
name: 'child_app',
|
||||
description: 't2',
|
||||
type: 'web',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
} as EntityDict['system']['CreateSingle'], context, {});
|
||||
await context.commit();
|
||||
|
||||
// // 查询: application.name 以 system.name 的前缀开头
|
||||
const r1 = await context.select('application', {
|
||||
data: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
system: { id: 1, name: 1 }
|
||||
},
|
||||
filter: {
|
||||
systemId,
|
||||
$expr: {
|
||||
$startsWith: [
|
||||
{ '#attr': 'name' },
|
||||
{
|
||||
'#refAttr': 'name',
|
||||
'#refId': 'node-1'
|
||||
}
|
||||
]
|
||||
},
|
||||
system: {
|
||||
'#id': 'node-1'
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
|
||||
assert(r1.length === 1, `Cross-entity expression failed, expected 1, got ${r1.length}`);
|
||||
assert(r1[0].id === appId1, `Expected appId1, got ${r1[0].id}`);
|
||||
});
|
||||
|
||||
// ==================== $mod 运算符测试 ====================
|
||||
|
||||
it('[10.1]math expression $mod', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
const id3 = v4();
|
||||
|
||||
await store.operate('user', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{ id: id1, name: 'user1', nickname: 'n1', idState: 'unverified', userState: 'normal' },
|
||||
{ id: id2, name: 'user2', nickname: 'n2', idState: 'unverified', userState: 'normal' },
|
||||
{ id: id3, name: 'user3', nickname: 'n3', idState: 'unverified', userState: 'normal' },
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $mod: $$seq$$ % 2 = 0 (偶数序列号)
|
||||
const r1 = await context.select('user', {
|
||||
data: { id: 1, $$seq$$: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2, id3] },
|
||||
$expr: {
|
||||
$eq: [
|
||||
{ $mod: [{ '#attr': '$$seq$$' }, 2] },
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
// 偶数序列号的数量取决于插入顺序,这里只检查查询能正常执行
|
||||
assert(r1.length >= 0, `$mod expression failed`);
|
||||
|
||||
// 测试 $mod: $$seq$$ % 3 = 1
|
||||
const r2 = await context.select('user', {
|
||||
data: { id: 1, $$seq$$: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2, id3] },
|
||||
$expr: {
|
||||
$eq: [
|
||||
{ $mod: [{ '#attr': '$$seq$$' }, 3] },
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length >= 0, `$mod with 3 failed`);
|
||||
});
|
||||
|
||||
|
||||
// ==================== 聚合与普通查询组合测试 ====================
|
||||
|
||||
it('[20.2]select with nested aggregation', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const systemId1 = v4();
|
||||
const systemId2 = v4();
|
||||
|
||||
await store.operate('system', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: systemId1,
|
||||
name: 'agg_system1',
|
||||
description: 'test',
|
||||
folder: '/test1',
|
||||
platformId: 'p1',
|
||||
application$system: [
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1_1', description: 't', type: 'web' } },
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1_2', description: 't', type: 'web' } },
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1_3', description: 't', type: 'wechatMp' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: systemId2,
|
||||
name: 'agg_system2',
|
||||
description: 'test',
|
||||
folder: '/test2',
|
||||
platformId: 'p1',
|
||||
application$system: [
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app2_1', description: 't', type: 'web' } },
|
||||
]
|
||||
}
|
||||
]
|
||||
} as EntityDict['system']['CreateMulti'], context, {});
|
||||
await context.commit();
|
||||
|
||||
// 查询 system 并附带 application 聚合统计
|
||||
const r1 = await context.select('system', {
|
||||
data: {
|
||||
id: 1,
|
||||
name: 1,
|
||||
application$system$$aggr: {
|
||||
$entity: 'application',
|
||||
data: {
|
||||
'#aggr': { type: 1 },
|
||||
'#count-1': { id: 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
id: { $in: [systemId1, systemId2] }
|
||||
}
|
||||
}, {});
|
||||
|
||||
assert(r1.length === 2, `Nested aggregation query failed`);
|
||||
|
||||
const sys1 = r1.find(s => s.id === systemId1);
|
||||
assert(sys1, `System1 not found`);
|
||||
assert(sys1.application$system$$aggr && sys1.application$system$$aggr.length === 2, `System1 should have 2 type groups`);
|
||||
|
||||
const webCount = sys1.application$system$$aggr.find((a: any) => a['#data']?.type === 'web');
|
||||
assert(webCount && webCount['#count-1'] === 2, `System1 web count should be 2`);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,811 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
// ==================== 日期函数测试 ====================
|
||||
|
||||
it('[6.1]date expression $year $month $day', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
// 2024-06-15 14:30:45
|
||||
const specificDate = new Date('2024-06-15T14:30:45.000Z').valueOf();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: specificDate,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $year
|
||||
const r1 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $year: { '#attr': 'refreshedAt' } }, 2024] }
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `$year failed`);
|
||||
|
||||
// 测试 $month
|
||||
const r2 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $month: { '#attr': 'refreshedAt' } }, 6] }
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `$month failed`);
|
||||
|
||||
// 测试 $day
|
||||
const r3 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $day: { '#attr': 'refreshedAt' } }, 15] }
|
||||
}
|
||||
}, {});
|
||||
assert(r3.length === 1, `$day failed`);
|
||||
});
|
||||
|
||||
it('[6.2]date expression $dayOfMonth $dayOfWeek $dayOfYear', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
// 2024-06-15 是星期六,是一年中的第167天
|
||||
const specificDate = new Date('2024-06-15T14:30:45.000Z').valueOf();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: specificDate,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
|
||||
// 测试 $dayOfMonth
|
||||
const r1 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $dayOfMonth: { '#attr': 'refreshedAt' } }, 15] }
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `$dayOfMonth failed`);
|
||||
|
||||
// 测试 $dayOfWeek (MySQL: 1=周日, 7=周六; 2024-06-15是周六)
|
||||
const r2 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $dayOfWeek: { '#attr': 'refreshedAt' } }, 7] }
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `$dayOfWeek failed`);
|
||||
|
||||
// 测试 $dayOfYear (2024-06-15 是第167天)
|
||||
const r3 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $dayOfYear: { '#attr': 'refreshedAt' } }, 167] }
|
||||
}
|
||||
}, {});
|
||||
assert(r3.length === 1, `$dayOfYear failed`);
|
||||
});
|
||||
|
||||
it('[6.3]date expression $weekday $weekOfYear', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
// 2024-06-15 是第24周
|
||||
const specificDate = new Date('2024-06-15T14:30:45.000Z').valueOf();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: specificDate,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $weekday (MySQL WEEKDAY: 0=周一, 6=周日; 2024-06-15是周六=5)
|
||||
const r1 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $weekday: { '#attr': 'refreshedAt' } }, 5] }
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `$weekday failed`);
|
||||
|
||||
// 测试 $weekOfYear
|
||||
const r2 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: { $eq: [{ $weekOfYear: { '#attr': 'refreshedAt' } }, 24] }
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `$weekOfYear failed`);
|
||||
});
|
||||
|
||||
it('[6.4]$dateDiff with different units', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const now = Date.now();
|
||||
const oneYearAgo = now - 365 * 24 * 3600 * 1000;
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: oneYearAgo,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试年份差异
|
||||
const r1 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{ $dateDiff: [now, { '#attr': 'refreshedAt' }, 'y'] },
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `$dateDiff year failed`);
|
||||
|
||||
// 测试月份差异
|
||||
const r2 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{ $dateDiff: [now, { '#attr': 'refreshedAt' }, 'M'] },
|
||||
11
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `$dateDiff month failed`);
|
||||
});
|
||||
|
||||
it('[6.5]$dateFloor with different units', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
// 2024-06-15 14:30:45.123
|
||||
const specificDate = new Date('2024-06-15T14:30:45.123Z').valueOf();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: specificDate,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
//TODO: 暂不支持
|
||||
// // 测试向下取整到月 - 应该是 2024-06-01
|
||||
const startOfMonth = new Date('2024-06-01T00:00:00.000Z').valueOf();
|
||||
const r1 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{ $dateFloor: [{ '#attr': 'refreshedAt' }, 'M'] },
|
||||
startOfMonth
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `$dateFloor month failed: Expected 1, got ${r1.length}`);
|
||||
|
||||
// 测试向下取整到年 - 应该是 2024-01-01
|
||||
const startOfYear = new Date('2024-01-01T00:00:00.000Z').valueOf();
|
||||
const r2 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{ $dateFloor: [{ '#attr': 'refreshedAt' }, 'y'] },
|
||||
startOfYear
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `$dateFloor year failed`);
|
||||
});
|
||||
|
||||
// TODO: 暂不支持
|
||||
it('[6.6]$dateCeil with different units', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
// 2024-06-15 14:30:45.123
|
||||
const specificDate = new Date('2024-06-15T14:30:45.123Z').valueOf();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: specificDate,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试向上取整到月 - 应该是 2024-07-01
|
||||
const startOfNextMonth = new Date('2024-07-01T00:00:00.000Z').valueOf();
|
||||
const r1 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$lte: [
|
||||
{ $dateCeil: [{ '#attr': 'refreshedAt' }, 'M'] },
|
||||
startOfNextMonth
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `$dateCeil month failed`);
|
||||
|
||||
// 测试向上取整到年 - 应该是 2025-01-01
|
||||
const startOfNextYear = new Date('2025-01-01T00:00:00.000Z').valueOf();
|
||||
const r2 = await context.select('token', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$lte: [
|
||||
{ $dateCeil: [{ '#attr': 'refreshedAt' }, 'y'] },
|
||||
startOfNextYear
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `$dateCeil year failed`);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('[1.14]$dateDiff expression', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const now = Date.now();
|
||||
const oneHourAgo = now - 3600 * 1000; // 1小时前
|
||||
const oneDayAgo = now - 24 * 3600 * 1000; // 1天前
|
||||
const oneMonthAgo = now - 30 * 24 * 3600 * 1000; // 约30天前
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
const id3 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: oneHourAgo,
|
||||
value: v4(),
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: oneDayAgo,
|
||||
value: v4(),
|
||||
},
|
||||
{
|
||||
id: id3,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: oneMonthAgo,
|
||||
value: v4(),
|
||||
}
|
||||
]
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
|
||||
// 测试秒级差异
|
||||
const r1 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$dateDiff: [
|
||||
now,
|
||||
{ '#attr': 'refreshedAt' },
|
||||
's'
|
||||
]
|
||||
},
|
||||
3500 // 约1小时 = 3600秒,给一些误差
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row for seconds diff, got ${r1.length}`);
|
||||
|
||||
// 测试分钟级差异
|
||||
const r2 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$dateDiff: [
|
||||
now,
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'm'
|
||||
]
|
||||
},
|
||||
55 // 约1小时 = 60分钟
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `Expected 1 row for minutes diff, got ${r2.length}`);
|
||||
|
||||
// 测试小时级差异
|
||||
const r3 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id2,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$dateDiff: [
|
||||
now,
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'h'
|
||||
]
|
||||
},
|
||||
23 // 约1天 = 24小时
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r3.length === 1, `Expected 1 row for hours diff, got ${r3.length}`);
|
||||
|
||||
// 测试天级差异
|
||||
const r4 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id3,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$dateDiff: [
|
||||
now,
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'd'
|
||||
]
|
||||
},
|
||||
25 // 约30天
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r4.length === 1, `Expected 1 row for days diff, got ${r4.length}`);
|
||||
});
|
||||
|
||||
it('[1.15]$dateFloor expression', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
// 创建一个特定时间的记录: 2024-06-15 14:30:45
|
||||
const specificTime = new Date('2024-06-15T14:30:45.123Z').valueOf();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: specificTime,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
|
||||
// 测试向下取整到分钟 - 应该得到 14:30:00
|
||||
const startOfMinute = new Date('2024-06-15T14:30:00.000Z').valueOf();
|
||||
const endOfMinute = new Date('2024-06-15T14:31:00.000Z').valueOf();
|
||||
|
||||
const r1 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$and: [
|
||||
{
|
||||
$gte: [
|
||||
{
|
||||
$dateFloor: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'm'
|
||||
]
|
||||
},
|
||||
startOfMinute
|
||||
]
|
||||
},
|
||||
{
|
||||
$lt: [
|
||||
{
|
||||
$dateFloor: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'm'
|
||||
]
|
||||
},
|
||||
endOfMinute
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row for minute floor, got ${r1.length}`);
|
||||
|
||||
// 测试向下取整到小时 - 应该得到 14:00:00
|
||||
const startOfHour = new Date('2024-06-15T14:00:00.000Z').valueOf();
|
||||
const endOfHour = new Date('2024-06-15T15:00:00.000Z').valueOf();
|
||||
|
||||
const r2 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$and: [
|
||||
{
|
||||
$gte: [
|
||||
{
|
||||
$dateFloor: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'h'
|
||||
]
|
||||
},
|
||||
startOfHour
|
||||
]
|
||||
},
|
||||
{
|
||||
$lt: [
|
||||
{
|
||||
$dateFloor: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'h'
|
||||
]
|
||||
},
|
||||
endOfHour
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `Expected 1 row for hour floor, got ${r2.length}`);
|
||||
|
||||
// 测试向下取整到天 - 应该得到 2024-06-15 00:00:00
|
||||
const startOfDay = new Date('2024-06-15T00:00:00.000Z').valueOf();
|
||||
const endOfDay = new Date('2024-06-16T00:00:00.000Z').valueOf();
|
||||
|
||||
const r3 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$and: [
|
||||
{
|
||||
$gte: [
|
||||
{
|
||||
$dateFloor: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'd'
|
||||
]
|
||||
},
|
||||
startOfDay
|
||||
]
|
||||
},
|
||||
{
|
||||
$lt: [
|
||||
{
|
||||
$dateFloor: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'd'
|
||||
]
|
||||
},
|
||||
endOfDay
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r3.length === 1, `Expected 1 row for day floor, got ${r3.length}`);
|
||||
});
|
||||
|
||||
it('[1.16]$dateCeil expression', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
// 创建一个特定时间的记录: 2024-06-15 14:30:45
|
||||
const specificTime = new Date('2024-06-15T14:30:45.123Z').valueOf();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: specificTime,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
|
||||
// 测试向上取整到分钟 - 应该得到 14:31:00
|
||||
const ceilMinute = new Date('2024-06-15T14:31:00.000Z').valueOf();
|
||||
|
||||
const r1 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$dateCeil: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'm'
|
||||
]
|
||||
},
|
||||
ceilMinute
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row for minute ceil, got ${r1.length}`);
|
||||
|
||||
// 测试向上取整到小时 - 应该得到 15:00:00
|
||||
const ceilHour = new Date('2024-06-15T15:00:00.000Z').valueOf();
|
||||
|
||||
const r2 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$dateCeil: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'h'
|
||||
]
|
||||
},
|
||||
ceilHour
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `Expected 1 row for hour ceil, got ${r2.length}`);
|
||||
|
||||
// 测试向上取整到天 - 应该得到 2024-06-16 00:00:00
|
||||
const ceilDay = new Date('2024-06-16T00:00:00.000Z').valueOf();
|
||||
|
||||
const r3 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
refreshedAt: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$dateCeil: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
'd'
|
||||
]
|
||||
},
|
||||
ceilDay
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r3.length === 1, `Expected 1 row for day ceil, got ${r3.length}`);
|
||||
});
|
||||
|
||||
it('[1.18]date expression with constants', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const now = Date.now();
|
||||
const id1 = v4();
|
||||
|
||||
await store.operate('token', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
env: { type: 'web' } as any,
|
||||
applicationId: v4(),
|
||||
userId: v4(),
|
||||
playerId: v4(),
|
||||
refreshedAt: now,
|
||||
value: v4(),
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
|
||||
// 测试 $dateDiff 使用常量日期
|
||||
const yesterday = now - 24 * 3600 * 1000;
|
||||
|
||||
const r1 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{
|
||||
$dateDiff: [
|
||||
now,
|
||||
yesterday,
|
||||
'd'
|
||||
]
|
||||
},
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row for constant date diff, got ${r1.length}`);
|
||||
|
||||
// 测试 $dateDiff 混合使用属性和常量
|
||||
const r2 = await context.select('token', {
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$lt: [
|
||||
{
|
||||
$dateDiff: [
|
||||
{ '#attr': 'refreshedAt' },
|
||||
yesterday,
|
||||
'h'
|
||||
]
|
||||
},
|
||||
48 // 应该在48小时以内
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `Expected 1 row for mixed date diff, got ${r2.length}`);
|
||||
});
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,698 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
it('[1.10]json insert/select', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id = await generateNewIdAsync();
|
||||
await context.operate('house', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id,
|
||||
areaId: 'area1',
|
||||
district: 'district1',
|
||||
ownerId: 'owner1',
|
||||
size: 123.4567,
|
||||
data: {
|
||||
rooms: 3,
|
||||
features: ['garden', 'garage'],
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'Metropolis',
|
||||
zip: '12345'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
|
||||
const result = await context.select('house', {
|
||||
data: {
|
||||
id: 1,
|
||||
// TODO: 在projection中支持json字段的部分展开,但是展开后返回值都为字符串!!!
|
||||
data: 1,
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
},
|
||||
}, {});
|
||||
|
||||
assert(result.length === 1, `Expected 1 result, got ${result.length}`);
|
||||
assert((result[0] as any).data.rooms === 3, `Expected rooms to be 3, got ${(result[0] as any).data.rooms}`);
|
||||
assert.deepStrictEqual((result[0] as any).data.features, ['garden', 'garage'], `Expected features to match`);
|
||||
assert.deepStrictEqual((result[0] as any).data.address, {
|
||||
street: '123 Main St',
|
||||
city: 'Metropolis',
|
||||
zip: '12345',
|
||||
}, `Expected address to match`);
|
||||
});
|
||||
|
||||
it('[1.11]json as filter', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id = await generateNewIdAsync();
|
||||
await store.operate('oper', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id,
|
||||
action: 'test',
|
||||
data: {
|
||||
name: 'xc',
|
||||
books: [{
|
||||
title: 'mathmatics',
|
||||
price: 1,
|
||||
}, {
|
||||
title: 'english',
|
||||
price: 2,
|
||||
}],
|
||||
},
|
||||
targetEntity: 'bbb',
|
||||
bornAt: 111,
|
||||
iState: 'normal',
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
books: [undefined, {
|
||||
title: 1,
|
||||
price: 1,
|
||||
}],
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
data: {
|
||||
name: 'xc',
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
const row2 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
books: [undefined, {
|
||||
title: 1,
|
||||
price: 1,
|
||||
}],
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
data: {
|
||||
name: 'xc2',
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
// console.log(JSON.stringify(row));
|
||||
assert(row.length === 1, JSON.stringify(row));
|
||||
assert(row2.length === 0, JSON.stringify(row2));
|
||||
});
|
||||
|
||||
|
||||
it('[1.11.2]json filter on top level', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id = await generateNewIdAsync();
|
||||
await store.operate('actionAuth', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id,
|
||||
pathId: await generateNewIdAsync(),
|
||||
deActions: ['1.12'],
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
|
||||
const row = await store.select('actionAuth', {
|
||||
data: {
|
||||
id: 1,
|
||||
deActions: 1,
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
deActions: {
|
||||
$overlaps: '1.12',
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
|
||||
const row2 = await store.select('actionAuth', {
|
||||
data: {
|
||||
id: 1,
|
||||
deActions: 1,
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
deActions: {
|
||||
$contains: ['1.13333'],
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
// console.log(JSON.stringify(row));
|
||||
assert(row.length === 1, JSON.stringify(row));
|
||||
console.log(JSON.stringify(row));
|
||||
assert(row2.length === 0, JSON.stringify(row2));
|
||||
|
||||
const row3 = await store.select('actionAuth', {
|
||||
data: {
|
||||
id: 1,
|
||||
deActions: 1,
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
deActions: {
|
||||
$exists: true,
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
assert(row3.length === 1);
|
||||
const row4 = await store.select('actionAuth', {
|
||||
data: {
|
||||
id: 1,
|
||||
deActions: 1,
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
deActions: {
|
||||
$exists: false,
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
assert(row4.length === 0);
|
||||
});
|
||||
|
||||
it('[1.11.3]json escape', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id = await generateNewIdAsync();
|
||||
await store.operate('oper', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id,
|
||||
action: 'test',
|
||||
data: {
|
||||
$or: [{
|
||||
name: 'xc',
|
||||
}, {
|
||||
name: {
|
||||
$includes: 'xc',
|
||||
}
|
||||
}],
|
||||
},
|
||||
targetEntity: 'bbb',
|
||||
bornAt: 123,
|
||||
iState: 'normal',
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
data: {
|
||||
'.$or': {
|
||||
$contains: {
|
||||
name: 'xc',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
const row2 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
data: {
|
||||
'.$or': [
|
||||
{
|
||||
name: 'xc',
|
||||
},
|
||||
{
|
||||
name: {
|
||||
'.$includes': 'xc',
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
assert(row.length === 1);
|
||||
assert(row2.length === 1);
|
||||
});
|
||||
|
||||
it('[1.12]complicated json filter', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id = await generateNewIdAsync();
|
||||
await store.operate('oper', {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id,
|
||||
action: 'test',
|
||||
data: {
|
||||
name: 'xcc',
|
||||
price: [100, 400, 1000],
|
||||
},
|
||||
targetEntity: 'bbb',
|
||||
bornAt: 123,
|
||||
iState: 'normal',
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: [undefined, 400],
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row2 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: [undefined, 200],
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row3 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: [undefined, {
|
||||
$gt: 300,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row4 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: {
|
||||
$contains: [200, 500],
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row5 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: {
|
||||
$contains: [100, 400],
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row6 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: {
|
||||
$contains: ['xc'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row7 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
name: {
|
||||
$includes: 'xc',
|
||||
},
|
||||
price: {
|
||||
$overlaps: [200, 400, 800],
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
/**
|
||||
* 带$or的查询
|
||||
*/
|
||||
process.env.NODE_ENV = 'development';
|
||||
const row8 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
$or: [
|
||||
{
|
||||
name: {
|
||||
$includes: 'xc',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
$includes: 'xzw',
|
||||
}
|
||||
}
|
||||
],
|
||||
price: {
|
||||
$overlaps: [200, 400, 800],
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
const row9 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: {
|
||||
$length: 3,
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
const row10 = await store.select('oper', {
|
||||
data: {
|
||||
id: 1,
|
||||
data: {
|
||||
name: 1,
|
||||
price: 1,
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
data: {
|
||||
price: {
|
||||
$length: {
|
||||
$gt: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}, context, {});
|
||||
|
||||
await context.commit();
|
||||
assert(row.length === 1);
|
||||
assert(row2.length === 0);
|
||||
assert(row3.length === 1);
|
||||
assert(row4.length === 0);
|
||||
assert(row5.length === 1);
|
||||
assert(row6.length === 0);
|
||||
assert(row7.length === 1);
|
||||
assert(row8.length === 1);
|
||||
assert(row9.length === 1);
|
||||
assert(row10.length === 0);
|
||||
// console.log(JSON.stringify(row7));
|
||||
});
|
||||
|
||||
|
||||
// ==================== JSON 操作符补充测试 ====================
|
||||
|
||||
it('[9.1]json $exists operator', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
|
||||
await store.operate('oper', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
action: 'test1',
|
||||
data: { name: 'xc', age: 25 },
|
||||
targetEntity: 'test',
|
||||
bornAt: Date.now(),
|
||||
iState: 'normal',
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
action: 'test2',
|
||||
data: { name: 'zz' }, // 没有 age 字段
|
||||
targetEntity: 'test',
|
||||
bornAt: Date.now(),
|
||||
iState: 'normal',
|
||||
}
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $exists: true - 检查 age 字段存在
|
||||
const r1 = await context.select('oper', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
data: { age: { $exists: true } }
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id1, `$exists true failed`);
|
||||
|
||||
// 测试 $exists: false - 检查 age 字段不存在
|
||||
const r2 = await context.select('oper', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
data: { age: { $exists: false } }
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1 && r2[0].id === id2, `$exists false failed`);
|
||||
|
||||
// 暂不支持这种写法
|
||||
// 测试 $exists: 'keyName' - 检查 JSON 对象是否包含指定键
|
||||
// const r3 = await context.select('oper', {
|
||||
// data: { id: 1 },
|
||||
// filter: {
|
||||
// id: { $in: [id1, id2] },
|
||||
// data: { $exists: 'age' }
|
||||
// }
|
||||
// }, {});
|
||||
// assert(r3.length === 1 && r3[0].id === id1, `$exists keyName failed`);
|
||||
});
|
||||
|
||||
it('[9.2]json nested $and $or in filter', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
const id3 = v4();
|
||||
|
||||
await store.operate('oper', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
action: 'test1',
|
||||
data: { name: 'alice', role: 'admin', level: 10 },
|
||||
targetEntity: 'test',
|
||||
bornAt: Date.now(),
|
||||
iState: 'normal',
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
action: 'test2',
|
||||
data: { name: 'bob', role: 'user', level: 5 },
|
||||
targetEntity: 'test',
|
||||
bornAt: Date.now(),
|
||||
iState: 'normal',
|
||||
},
|
||||
{
|
||||
id: id3,
|
||||
action: 'test3',
|
||||
data: { name: 'charlie', role: 'admin', level: 3 },
|
||||
targetEntity: 'test',
|
||||
bornAt: Date.now(),
|
||||
iState: 'normal',
|
||||
}
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试: (role = 'admin' AND level > 5) OR (role = 'user')
|
||||
const r1 = await context.select('oper', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2, id3] },
|
||||
data: {
|
||||
$or: [
|
||||
{
|
||||
$and: [
|
||||
{ role: 'admin' },
|
||||
{ level: { $gt: 5 } }
|
||||
]
|
||||
},
|
||||
{ role: 'user' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 2, `Nested $and $or failed, expected 2, got ${r1.length}`);
|
||||
const ids = r1.map(r => r.id);
|
||||
assert(ids.includes(id1) && ids.includes(id2), `Expected id1 and id2`);
|
||||
});
|
||||
|
||||
it('[9.3]json array index access in filter', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
|
||||
await store.operate('oper', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
action: 'test1',
|
||||
data: { scores: [90, 85, 95] },
|
||||
targetEntity: 'test',
|
||||
bornAt: Date.now(),
|
||||
iState: 'normal',
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
action: 'test2',
|
||||
data: { scores: [70, 75, 80] },
|
||||
targetEntity: 'test',
|
||||
bornAt: Date.now(),
|
||||
iState: 'normal',
|
||||
}
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试数组第一个元素 > 80
|
||||
const r1 = await context.select('oper', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
data: {
|
||||
scores: [{ $gt: 80 }]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id1, `Array index filter failed`);
|
||||
|
||||
// 测试数组第三个元素 = 95
|
||||
const r2 = await context.select('oper', {
|
||||
data: { id: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
data: {
|
||||
scores: [undefined, undefined, 95]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1 && r2[0].id === id1, `Array specific index filter failed`);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 数学运算符测试 ====================
|
||||
|
||||
it('[2.1]math expression $add', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 80.5,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '上海',
|
||||
size: 100.0,
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $add: size + 20 > 100
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$gt: [
|
||||
{
|
||||
$add: [{ '#attr': 'size' }, 20]
|
||||
},
|
||||
100
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 2, `Expected 2 rows, got ${r1.length}`);
|
||||
|
||||
// 测试多参数 $add: size + 10 + 20 > 120
|
||||
const r2 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$gt: [
|
||||
{
|
||||
$add: [{ '#attr': 'size' }, 10, 20]
|
||||
},
|
||||
120
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1 && r2[0].id === id2, `Expected id2, got ${r2.map(r => r.id)}`);
|
||||
});
|
||||
|
||||
it('[2.2]math expression $subtract', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 80.0,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '上海',
|
||||
size: 120.0,
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $subtract: size - 50 > 50
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$gt: [
|
||||
{
|
||||
$subtract: [{ '#attr': 'size' }, 50]
|
||||
},
|
||||
50
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id2, `Expected id2, got ${r1.map(r => r.id)}`);
|
||||
});
|
||||
|
||||
it('[2.3]math expression $multiply', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 50.0,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '上海',
|
||||
size: 100.0,
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $multiply: size * 2 >= 200
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$gte: [
|
||||
{
|
||||
$multiply: [{ '#attr': 'size' }, 2]
|
||||
},
|
||||
200
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id2, `Expected id2, got ${r1.map(r => r.id)}`);
|
||||
|
||||
// 测试多参数 $multiply: size * 2 * 3 > 500
|
||||
const r2 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$gt: [
|
||||
{
|
||||
$multiply: [{ '#attr': 'size' }, 2, 3]
|
||||
},
|
||||
500
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1 && r2[0].id === id2, `Expected id2, got ${r2.map(r => r.id)}`);
|
||||
});
|
||||
|
||||
it('[2.4]math expression $divide', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 80.0,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '上海',
|
||||
size: 200.0,
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $divide: size / 2 > 50
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$gt: [
|
||||
{
|
||||
$divide: [{ '#attr': 'size' }, 2]
|
||||
},
|
||||
50
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id2, `Expected id2, got ${r1.map(r => r.id)}`);
|
||||
});
|
||||
|
||||
it('[2.5]math expression $abs', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 80.0,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '上海',
|
||||
size: 120.0,
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $abs: abs(size - 100) <= 20
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$lte: [
|
||||
{
|
||||
$abs: {
|
||||
$subtract: [{ '#attr': 'size' }, 100]
|
||||
}
|
||||
},
|
||||
20
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 2, `Expected 2 rows, got ${r1.length}`);
|
||||
});
|
||||
|
||||
it('[2.6]math expression $round', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 80.567,
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $round: round(size, 1) = 80.6
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{
|
||||
$round: [{ '#attr': 'size' }, 1]
|
||||
},
|
||||
80.6
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row, got ${r1.length}`);
|
||||
|
||||
// 测试 $round: round(size, 0) = 81
|
||||
const r2 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{
|
||||
$round: [{ '#attr': 'size' }, 0]
|
||||
},
|
||||
81
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `Expected 1 row, got ${r2.length}`);
|
||||
});
|
||||
|
||||
it('[2.7]math expression $ceil and $floor', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 80.3,
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $ceil: ceil(size) = 81
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{ $ceil: { '#attr': 'size' } },
|
||||
81
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row for ceil, got ${r1.length}`);
|
||||
|
||||
// 测试 $floor: floor(size) = 80
|
||||
const r2 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{ $floor: { '#attr': 'size' } },
|
||||
80
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r2.length === 1, `Expected 1 row for floor, got ${r2.length}`);
|
||||
});
|
||||
|
||||
it('[2.8]math expression $pow', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 10.0,
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $pow: pow(size, 2) = 100
|
||||
const r1 = await context.select('house', {
|
||||
data: { id: 1, size: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{ $pow: [{ '#attr': 'size' }, 2] },
|
||||
100
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row, got ${r1.length}`);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 投影中的表达式测试 ====================
|
||||
|
||||
it('[11.1]expression in projection', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 100.0,
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 在投影中使用表达式计算 size * 2
|
||||
const r1 = await context.select('house', {
|
||||
data: {
|
||||
id: 1,
|
||||
size: 1,
|
||||
$expr: {
|
||||
$multiply: [{ '#attr': 'size' }, 2]
|
||||
}
|
||||
},
|
||||
filter: { id: id1 }
|
||||
}, {});
|
||||
|
||||
assert(r1.length === 1, `Expression in projection failed`);
|
||||
assert(r1[0].$expr == 200, `Expected $expr = 200, got ${r1[0].$expr}`);
|
||||
});
|
||||
|
||||
it('[11.2]multiple expressions in projection', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
await store.operate('house', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
areaId: v4(),
|
||||
ownerId: v4(),
|
||||
district: '杭州',
|
||||
size: 100.0,
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 多个表达式在投影中
|
||||
const r1 = await context.select('house', {
|
||||
data: {
|
||||
id: 1,
|
||||
size: 1,
|
||||
$expr1: {
|
||||
$multiply: [{ '#attr': 'size' }, 2]
|
||||
},
|
||||
$expr2: {
|
||||
$add: [{ '#attr': 'size' }, 50]
|
||||
},
|
||||
$expr3: {
|
||||
$gt: [{ '#attr': 'size' }, 80]
|
||||
}
|
||||
},
|
||||
filter: { id: id1 }
|
||||
}, {});
|
||||
|
||||
assert(r1.length === 1, `Multiple expressions failed`);
|
||||
assert(r1[0].$expr1 == 200, `Expected $expr1 = 200`);
|
||||
assert(r1[0].$expr2 == 150, `Expected $expr2 = 150`);
|
||||
assert(r1[0].$expr3 == true || r1[0].$expr3 == 1, `Expected $expr3 = true`);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 字符串操作测试 ====================
|
||||
|
||||
it('[3.1]string expression $endsWith', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('user', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
name: 'test_user',
|
||||
nickname: 'test', idState: 'unverified', userState: 'normal'
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
name: 'admin_role',
|
||||
nickname: 'admin', idState: 'unverified', userState: 'normal'
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $endsWith: name ends with '_user'
|
||||
const r1 = await context.select('user', {
|
||||
data: { id: 1, name: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$endsWith: [{ '#attr': 'name' }, '_user']
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id1, `Expected id1, got ${r1.map(r => r.id)}`);
|
||||
});
|
||||
|
||||
it('[3.2]string expression $includes', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
await store.operate('user', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: id1,
|
||||
name: 'test_admin_user',
|
||||
nickname: 'test', idState: 'unverified', userState: 'normal'
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
name: 'guest_role',
|
||||
nickname: 'guest', idState: 'unverified', userState: 'normal'
|
||||
},
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $includes: name includes 'admin'
|
||||
const r1 = await context.select('user', {
|
||||
data: { id: 1, name: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2] },
|
||||
$expr: {
|
||||
$includes: [{ '#attr': 'name' }, 'admin']
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1 && r1[0].id === id1, `Expected id1, got ${r1.map(r => r.id)}`);
|
||||
});
|
||||
|
||||
it('[3.3]string expression $concat', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
await store.operate('user', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: id1,
|
||||
name: 'hello',
|
||||
nickname: 'world', idState: 'unverified', userState: 'normal'
|
||||
}
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 $concat: concat(name, '_', nickname) = 'hello_world'
|
||||
const r1 = await context.select('user', {
|
||||
data: { id: 1, name: 1, nickname: 1 },
|
||||
filter: {
|
||||
id: id1,
|
||||
$expr: {
|
||||
$eq: [
|
||||
{ $concat: [{ '#attr': 'name' }, '_', { '#attr': 'nickname' }] },
|
||||
'hello_world'
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 1, `Expected 1 row, got ${r1.length}`);
|
||||
});
|
||||
|
||||
|
||||
// ==================== 字符串 $startsWith 在表达式中测试 ====================
|
||||
|
||||
it('[10.2]string $startsWith in expression', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const id1 = v4();
|
||||
const id2 = v4();
|
||||
const id3 = v4();
|
||||
|
||||
await store.operate('user', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{ id: id1, name: 'test_admin', nickname: 'test', idState: 'unverified', userState: 'normal' },
|
||||
{ id: id2, name: 'test_user', nickname: 'test', idState: 'unverified', userState: 'normal' },
|
||||
{ id: id3, name: 'admin_role', nickname: 'admin1', idState: 'unverified', userState: 'normal' },
|
||||
]
|
||||
}, context, {});
|
||||
await context.commit();
|
||||
|
||||
// 测试 name 以 nickname 开头
|
||||
const r1 = await context.select('user', {
|
||||
data: { id: 1, name: 1, nickname: 1 },
|
||||
filter: {
|
||||
id: { $in: [id1, id2, id3] },
|
||||
$expr: {
|
||||
$startsWith: [
|
||||
{ '#attr': 'name' },
|
||||
{ '#attr': 'nickname' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
assert(r1.length === 2, `$startsWith with two attrs failed, expected 2, got ${r1.length}`);
|
||||
const ids = r1.map(r => r.id);
|
||||
assert(ids.includes(id1) && ids.includes(id2), `Expected id1 and id2`);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { TestContext } from "../Context";
|
||||
import { EntityDict } from "../test-app-domain";
|
||||
import { v4 } from 'uuid';
|
||||
import assert from 'assert';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
import { describe, it, before, after } from 'mocha';
|
||||
import { DbStore } from "../../lib/types/dbStore";
|
||||
|
||||
export default (storeGetter: () => DbStore<EntityDict, TestContext>) => {
|
||||
|
||||
// ==================== 子查询中的表达式测试 ====================
|
||||
|
||||
it('[12.1]expression in subquery filter', async () => {
|
||||
const store = storeGetter();
|
||||
const context = new TestContext(store);
|
||||
await context.begin();
|
||||
|
||||
const systemId1 = v4();
|
||||
const systemId2 = v4();
|
||||
|
||||
await store.operate('system', {
|
||||
id: v4(),
|
||||
action: 'create',
|
||||
data: [
|
||||
{
|
||||
id: systemId1,
|
||||
name: 'system_a',
|
||||
description: 'test',
|
||||
folder: '/a',
|
||||
application$system: [
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app_a1', description: 't', type: 'web' } },
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'app_a2', description: 't', type: 'web' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: systemId2,
|
||||
name: 'system_b',
|
||||
description: 'test',
|
||||
folder: '/b',
|
||||
application$system: [
|
||||
{ id: v4(), action: 'create', data: { id: v4(), name: 'other_b1', description: 't', type: 'web' } },
|
||||
]
|
||||
}
|
||||
]
|
||||
} as EntityDict['system']['CreateMulti'], context, {});
|
||||
await context.commit();
|
||||
|
||||
// 查询: 存在以 'app_' 开头的 application 的 system
|
||||
const r1 = await context.select('system', {
|
||||
data: { id: 1, name: 1 },
|
||||
filter: {
|
||||
id: { $in: [systemId1, systemId2] },
|
||||
application$system: {
|
||||
name: { $startsWith: 'app_' }
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
|
||||
assert(r1.length === 1 && r1[0].id === systemId1, `Subquery with expression failed`);
|
||||
});
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue