fix: 修复了$contains的问题。

feat: 拆分了所有的测试用例
fix: 修复了部分测试用例的问题
This commit is contained in:
Pan Qiancheng 2026-01-01 22:07:18 +08:00
parent e9059b8514
commit 5858dd7db0
32 changed files with 5610 additions and 5181 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View File

@ -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];

View File

@ -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;

View File

@ -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 ';
}

View File

@ -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"
}
}

190
script/build.js Normal file
View File

@ -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);

View File

@ -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`);

View File

@ -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);

View File

@ -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 {

View File

@ -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 ';
}

View File

@ -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',
}
}
}
};

55
test/entities/Area.ts Normal file
View File

@ -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',
}
}
}
};

View File

@ -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: '数据',
},
},
},
}
};

24
test/entities/Platform.ts Normal file
View File

@ -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',
},
},
}
};

31
test/entities/System.ts Normal file
View File

@ -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: '平台',
},
},
}
};

91
test/entities/Token.ts Normal file
View File

@ -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',
},
},
},
};

176
test/entities/User.ts Normal file
View File

@ -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',
},
},
},
};

View File

@ -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 () => {

View File

@ -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 () => {

View File

@ -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',
}
}
]

323
test/testcase/aggr.ts Normal file
View File

@ -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`);
// });
}

1801
test/testcase/base.ts Normal file

File diff suppressed because it is too large Load Diff

119
test/testcase/bool.ts Normal file
View File

@ -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}`);
});
}

85
test/testcase/compare.ts Normal file
View File

@ -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}`);
});
}

340
test/testcase/complax.ts Normal file
View File

@ -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`);
});
}

811
test/testcase/date.ts Normal file
View File

@ -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

698
test/testcase/json.ts Normal file
View File

@ -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`);
});
}

423
test/testcase/math.ts Normal file
View File

@ -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}`);
});
}

View File

@ -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`);
});
}

163
test/testcase/string.ts Normal file
View File

@ -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`);
});
}

62
test/testcase/subquery.ts Normal file
View File

@ -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`);
});
}