oak-db/test/testcase/index.ts

5117 lines
158 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { MysqlStore } from "../../lib/MySQL/store";
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 { WebConfig } from '../test-app-domain/Application/Schema';
import { describe, it, before, after } from '../utils/test';
import { DbStore } from "../../lib/types/dbStore";
export const tests = (storeGetter: () => DbStore<EntityDict, TestContext>) => {
it('test insert', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
await store.operate('user', {
id: v4(),
action: 'create',
data: [
{
id: v4(),
name: 'xc',
nickname: 'xc',
},
{
id: v4(),
name: 'zz',
nickname: 'zzz',
}
]
}, context, {});
await context.commit();
});
it('test cascade insert', async () => {
const store = storeGetter();
const context = new TestContext(store);
await store.operate('user', {
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'xxxc',
nickname: 'ddd',
token$player: [{
id: v4(),
action: 'create',
data: {
id: v4(),
env: {
type: 'web',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
refreshedAt: Date.now(),
value: v4(),
}
}]
} as EntityDict['user']['CreateSingle']['data']
}, context, {});
});
it('test update', async () => {
const store = storeGetter();
const context = new TestContext(store);
const tokenId = v4();
await context.begin();
await store.operate('user', {
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'xxxc',
nickname: 'ddd',
token$player: [{
id: v4(),
action: 'create',
data: {
id: tokenId,
env: {
type: 'web',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
refreshedAt: Date.now(),
value: v4(),
}
}]
}
} as EntityDict['user']['CreateSingle'], context, {});
await store.operate('token', {
id: v4(),
action: 'update',
filter: {
id: tokenId,
},
data: {
player: {
id: v4(),
action: 'activate',
data: {
name: 'xcxcxc0903'
},
}
}
}, context, {});
await context.commit();
});
it('test cascade update', async () => {
const store = storeGetter();
const context = new TestContext(store);
const userId = v4();
const tokenId1 = v4();
const tokenId2 = v4();
await context.begin();
await context.operate('user', {
id: v4(),
action: 'create',
data: {
id: userId,
name: 'xxxc',
nickname: 'ddd',
token$player: [{
id: v4(),
action: 'create',
data: {
id: tokenId1,
env: {
type: 'web',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
refreshedAt: Date.now(),
value: v4(),
}
}]
}
} as EntityDict['user']['CreateSingle'], {});
await context.operate('token', {
id: v4(),
action: 'create',
data: {
id: tokenId2,
env: {
type: 'web',
},
applicationId: v4(),
userId: v4(),
playerId: v4(),
entity: 'mobile',
entityId: v4(),
refreshedAt: Date.now(),
value: v4(),
} as EntityDict['token']['CreateSingle']['data'],
}, {});
await context.commit();
// cascade update token of userId
await context.operate('user', {
id: v4(),
action: 'update',
data: {
name: 'xc',
token$player: [{
id: v4(),
action: 'update',
data: {
entity: 'email',
}
}]
},
filter: {
id: userId,
},
}, {});
const [row] = await context.select('token', {
data: {
id: 1,
entity: 1,
entityId: 1,
},
filter: {
id: tokenId2,
}
}, {});
assert(row.entity === 'mobile');
});
it('test delete', async () => {
const store = storeGetter();
const context = new TestContext(store);
const tokenId = v4();
await context.begin();
await store.operate('user', {
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'xxxc',
nickname: 'ddd',
token$player: [{
id: v4(),
action: 'create',
data: {
id: tokenId,
env: {
type: 'server',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
refreshedAt: Date.now(),
value: v4(),
}
}]
}
}, context, {});
await store.operate('token', {
id: v4(),
action: 'remove',
filter: {
id: tokenId,
},
data: {
player: {
id: v4(),
action: 'update',
data: {
name: 'xcxcxc0902'
},
}
}
}, context, {});
await context.commit();
});
it('test delete2', async () => {
const store = storeGetter();
// 这个例子暂在mysql上过不去先放着吧
const context = new TestContext(store);
const tokenId = v4();
await context.begin();
await store.operate('user', {
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'xxxc',
nickname: 'ddd',
token$player: [{
id: v4(),
action: 'create',
data: {
id: tokenId,
env: {
type: 'server',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
refreshedAt: Date.now(),
value: v4(),
}
}]
}
}, context, {});
await store.operate('user', {
id: v4(),
action: 'remove',
filter: {
id: tokenId,
},
data: {
ref: {
id: await generateNewIdAsync(),
action: 'remove',
data: {},
}
},
}, context, {});
await context.commit();
});
it('test decimal', async () => {
const store = storeGetter();
const id = v4();
const context = new TestContext(store);
await context.begin();
await store.operate('house', {
id: v4(),
action: 'create',
data: [
{
id,
areaId: 'xc',
ownerId: 'xc',
district: '杭州',
size: 77.5,
},
]
}, context, {});
await context.commit();
const [house] = await store.select('house', {
data: {
id: 1,
size: 1,
},
filter: {
id,
},
}, context, {});
assert(typeof house.size === 'number');
});
it('[1.1]子查询', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
await store.operate('user', {
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'xc',
nickname: 'xc',
}
}, context, {});
process.env.NODE_ENV = 'development';
const rows = await store.select('user', {
data: {
id: 1,
name: 1,
nickname: 1,
},
filter: {
token$user: {
}
},
}, context, {});
process.env.NODE_ENV = undefined;
// console.log(rows);
assert(rows.length === 0);
await context.commit();
});
it('[1.2]行内属性上的表达式', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
const id = v4();
await store.operate('user', {
id: v4(),
action: 'create',
data: {
id,
name: 'xc',
nickname: 'xc',
}
}, context, {});
const id2 = v4();
await store.operate('user', {
id: v4(),
action: 'create',
data: {
id: id2,
name: 'xzw',
nickname: 'xzw22',
}
}, context, {});
process.env.NODE_ENV = 'development';
const users = await store.select('user', {
data: {
id: 1,
name: 1,
nickname: 1,
},
filter: {
id: {
$in: [id, id2],
},
$expr: {
$eq: [{
'#attr': 'name',
}, {
"#attr": 'nickname',
}]
},
},
}, context, {});
const users2 = await store.select('user', {
data: {
id: 1,
name: 1,
nickname: 1,
},
filter: {
id: {
$in: [id, id2],
},
$expr: {
$eq: [{
$mod: [
{
'#attr': '$$seq$$',
},
2,
],
}, 0]
},
},
}, context, {});
process.env.NODE_ENV = undefined;
assert(users.length === 1);
assert(users2.length === 1);
await context.commit();
});
it('[1.3]跨filter结点的表达式', async () => {
const store = storeGetter();
const context = new TestContext(store);
const id1 = v4();
const id2 = v4();
await context.begin();
await store.operate('application', {
id: v4(),
action: 'create',
data: [{
id: id1,
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
system: {
id: v4(),
action: 'create',
data: {
id: 'bbb',
name: 'systest',
description: 'aaaaa',
config: {},
folder: '/systest',
platformId: 'platform-111',
} as EntityDict['system']['CreateSingle']['data']
},
}, {
id: id2,
name: 'test2',
description: 'ttttt2',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
system: {
id: v4(),
action: 'create',
data: {
id: 'ccc',
name: 'test2',
description: 'aaaaa2',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
}
},
}]
}, context, {});
const applications = await store.select('application', {
data: {
id: 1,
name: 1,
systemId: 1,
system: {
id: 1,
name: 1,
}
},
filter: {
$expr: {
$startsWith: [
{
"#refAttr": 'name',
"#refId": 'node-1',
},
{
"#attr": 'name',
}
]
},
system: {
"#id": 'node-1',
},
id: id2,
},
sorter: [
{
$attr: {
system: {
name: 1,
}
},
$direction: 'asc',
}
]
}, context, {});
console.log(applications);
assert(applications.length === 1 && applications[0].id === id2);
await context.commit();
});
it('[1.4]跨filter子查询的表达式', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
await store.operate('application', {
id: v4(),
action: 'create',
data: [{
id: 'aaaa',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
system: {
id: v4(),
action: 'create',
data: {
id: 'bbbb',
name: 'systest',
description: 'aaaaa',
config: {
App: {},
},
folder: '/systest',
platformId: 'platform-111',
}
},
}, {
id: 'aaaa2',
name: 'test2',
description: 'ttttt2',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
system: {
id: v4(),
action: 'create',
data: {
id: 'cccc',
name: 'test2',
description: 'aaaaa2',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
}
},
}]
}, context, {});
process.env.NODE_ENV = 'development';
let systems = await store.select('system', {
data: {
id: 1,
name: 1,
},
filter: {
"#id": 'node-1',
$and: [
{
application$system: {
'#sqp': 'not in',
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
'#id': 'node-2',
}
}, {
id: {
$in: ['bbbb', 'cccc'],
}
}
]
},
sorter: [
{
$attr: {
name: 1,
},
$direction: 'asc',
}
]
}, context, {});
assert(systems.length === 1 && systems[0].id === 'bbbb');
systems = await store.select('system', {
data: {
id: 1,
name: 1,
},
filter: {
"#id": 'node-1',
$and: [
{
application$system: {
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
},
},
{
id: {
$in: ['bbbb', 'cccc'],
},
}
]
},
sorter: [
{
$attr: {
name: 1,
},
$direction: 'asc',
}
]
}, context, {});
process.env.NODE_ENV = undefined;
assert(systems.length === 1 && systems[0].id === 'cccc');
await context.commit();
});
it('[1.5]projection中的跨结点表达式', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
await store.operate('application', {
id: v4(),
action: 'create',
data: [{
id: 'aaa5',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
system: {
id: v4(),
action: 'create',
data: {
id: 'bbb5',
name: 'systest',
description: 'aaaaa',
config: {
App: {},
},
folder: '/systest',
platformId: 'platform-111',
}
},
}, {
id: 'aaa5-2',
name: 'test2',
description: 'ttttt2',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
system: {
id: v4(),
action: 'create',
data: {
id: 'ccc5',
name: 'test2',
description: 'aaaaa2',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
} as EntityDict['system']['CreateSingle']['data'],
},
}]
}, context, {});
let applications = await store.select('application', {
data: {
"#id": 'node-1',
id: 1,
name: 1,
system: {
id: 1,
name: 1,
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
}
},
filter: {
id: {
$in: ['aaa5', 'aaa5-2'],
},
},
}, context, {});
// console.log(applications);
assert(applications.length === 2);
applications.forEach(
(app) => {
assert(app.id === 'aaa5' && !(app.system!.$expr)
|| app.id === 'aaa5-2' && !!(app.system!.$expr));
}
);
const applications2 = await store.select('application', {
data: {
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
id: 1,
name: 1,
system: {
"#id": 'node-1',
id: 1,
name: 1,
}
},
filter: {
id: {
$in: ['aaa5', 'aaa5-2'],
},
},
}, context, {});
// console.log(applications2);
// assert(applications.length === 2);
applications2.forEach(
(app) => {
assert(app.id === 'aaa5' && !(app.$expr)
|| app.id === 'aaa5-2' && !!(app.$expr));
}
);
await context.commit();
});
// 这个貌似目前支持不了 by Xc
it('[1.6]projection中的一对多跨结点表达式', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
await store.operate('system', {
id: v4(),
action: 'create',
data: {
id: 'bbb6',
name: 'test2',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: 'aaa6',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
},
{
id: v4(),
action: 'create',
data: {
id: 'aaa6-2',
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}]
}
} as EntityDict['system']['CreateSingle'], context, {});
const systems = await store.select('system', {
data: {
"#id": 'node-1',
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
$expr2: {
'#refId': 'node-1',
"#refAttr": 'id',
}
}
},
},
}, context, {});
// console.log(systems);
assert(systems.length === 1);
const [system] = systems;
const { application$system: applications } = system;
assert(applications!.length === 2);
applications!.forEach(
(ele) => {
assert(ele.id === 'aaa' && ele.$expr === false && ele.$expr2 === 'bbb'
|| ele.id === 'aaa2' && ele.$expr === true && ele.$expr2 === 'bbb');
}
);
});
it('[1.7]事务性测试', 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: 'test2',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: 'aaa7',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}, {
id: v4(),
action: 'create',
data: {
id: 'aaa7-2',
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}]
}
}, context, {});
await context.commit();
await context.begin();
const systems = await store.select('system', {
data: {
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
systemId: 1,
}
},
},
filter: {
id: systemId,
},
}, context, {});
assert(systems.length === 1 && systems[0].application$system!.length === 2);
await store.operate('application', {
id: v4(),
action: 'remove',
data: {},
filter: {
id: 'aaa7',
}
}, context, {});
const systems2 = await store.select('system', {
data: {
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
systemId: 1,
}
},
},
filter: {
id: systemId,
},
}, context, {});
assert(systems2.length === 1 && systems2[0].application$system!.length === 1);
await context.rollback();
const systems3 = await store.select('system', {
data: {
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
systemId: 1,
}
},
},
filter: {
id: systemId,
},
}, context, {});
assert(systems3.length === 1 && systems3[0].application$system!.length === 2);
});
it('[1.8]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: 'test2',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
},
{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
},
}
}]
},
{
id: systemId2,
name: 'test2',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
},
}
},
{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
},
}
}]
}
]
} as EntityDict['system']['CreateMulti'], context, {});
await context.commit();
await context.begin();
const result = await store.aggregate('application', {
data: {
'#aggr': {
system: {
id: 1,
},
},
'#count-1': {
id: 1,
}
},
filter: {
systemId: {
$in: [systemId1, systemId2],
},
},
}, context, {});
await context.commit();
// console.log(result);
assert(result.length === 2);
result.forEach(
(row) => assert(row['#count-1'] === 2)
);
});
it('[1.9]test + 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: 'test2',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
},
{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}]
},
{
id: systemId2,
name: 'test2',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
},
{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}]
}
]
} as EntityDict['system']['CreateMulti'], context, {});
await context.commit();
await context.begin();
const result = await store.select('system', {
data: {
id: 1,
name: 1,
application$system$$aggr: {
$entity: 'application',
data: {
'#aggr': {
systemId: 1,
},
'#count-1': {
id: 1,
}
},
},
},
filter: {
id: {
$in: [systemId1, systemId2],
},
},
}, context, {});
await context.commit();
// console.log(result);
assert(result.length === 2);
result.forEach(
(row) => assert(row.application$system$$aggr?.length === 1 && row.application$system$$aggr[0]['#count-1'] === 2)
);
});
it('[1.10]json insert/select', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
let id = await generateNewIdAsync();
await context.operate('application', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id,
name: 'xuchang',
description: 'tt',
type: 'web',
systemId: 'system',
config: {
type: 'web',
passport: ['email', 'mobile'],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}, {});
let result = await context.select('application', {
data: {
id: 1,
name: 1,
config: {
passport: [undefined, 1],
wechat: {
appId: 1,
}
},
},
filter: {
id,
},
}, {});
// console.log(JSON.stringify(result));
id = await generateNewIdAsync();
await context.operate('application', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id,
name: 'xuchang',
description: 'tt',
type: 'web',
systemId: 'system',
config: {
type: 'web',
wechat: {
appId: 'aaaa\\nddddd',
appSecret: '',
},
passport: ['email', 'mobile'],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}, {});
result = await context.select('application', {
data: {
id: 1,
name: 1,
config: {
passport: [undefined, 1],
wechat: {
appId: 1,
}
},
},
filter: {
id,
},
}, {});
console.log((result[0].config as WebConfig)!.wechat!.appId);
});
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,
}
}, 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,
}
}, 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,
}
}, 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));
});
it('[1.13]sub query predicate', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
const id1 = await generateNewIdAsync();
const id2 = await generateNewIdAsync();
const id3 = await generateNewIdAsync();
const systems: EntityDict['system']['CreateSingle']['data'][] = [
{
id: id1,
name: 'test1.13-1',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test1.13-1-1',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
},
{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test1.13-1-2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}]
},
{
id: id2,
name: 'test1.13-2',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
application$system: [{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test1.13-2-1',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
},
{
id: v4(),
action: 'create',
data: {
id: v4(),
name: 'test1.13-2-2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
passport: [],
location: {
protocol: "http:",
hostname: '',
port: '',
},
},
}
}]
},
{
id: id3,
name: 'test1.13-3',
description: 'aaaaa',
config: {
App: {},
},
folder: '/test2',
platformId: 'platform-111',
}
];
await context.operate('system', {
id: v4(),
action: 'create',
data: systems,
}, {});
await context.commit();
const r1 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: id1,
application$system: {
}
}
}, {});
assert(r1.length === 1);
const r2 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: id1,
application$system: {
'#sqp': 'not in',
},
},
}, {});
assert(r2.length === 0);
const r22 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: id1,
application$system: {
name: {
$startsWith: 'test1.13-2',
},
'#sqp': 'not in',
},
},
}, {});
assert(r22.length === 1);
const r23 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: id1,
application$system: {
name: {
$startsWith: 'test1.13-2',
},
'#sqp': 'all',
},
},
}, {});
assert(r23.length === 0);
const r24 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: id1,
application$system: {
name: {
$startsWith: 'test1.13-2',
},
'#sqp': 'not all',
},
},
}, {});
assert(r24.length === 1);
const r3 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: id1,
application$system: {
'#sqp': 'all',
},
},
}, {});
assert(r3.length === 0);
const r4 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: id1,
application$system: {
'#sqp': 'not all',
},
},
}, {});
assert(r4.length === 1);
const r5 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: {
$in: [id1, id2, id3],
},
application$system: {
},
},
}, {});
assert(r5.length === 2);
assert(r5.map(ele => ele.id).includes(id1));
assert(r5.map(ele => ele.id).includes(id2));
const r6 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: {
$in: [id1, id2, id3],
},
application$system: {
'#sqp': 'not in',
},
},
}, {});
assert(r6.length === 1);
assert(r6.map(ele => ele.id).includes(id3));
const r7 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: {
$in: [id1, id2, id3],
},
application$system: {
'#sqp': 'all',
},
},
}, {});
assert(r7.length === 0);
const r8 = await context.select('system', {
data: {
id: 1,
},
filter: {
id: {
$in: [id1, id2, id3],
},
application$system: {
'#sqp': 'not all',
},
},
}, {});
assert(r8.length === 3);
assert(r8.map(ele => ele.id).includes(id1));
assert(r8.map(ele => ele.id).includes(id2));
assert(r8.map(ele => ele.id).includes(id3));
});
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(),
entity: 'mobile',
entityId: v4(),
refreshedAt: oneHourAgo,
value: v4(),
},
{
id: id2,
env: { type: 'web' } as any,
applicationId: v4(),
userId: v4(),
playerId: v4(),
entity: 'mobile',
entityId: v4(),
refreshedAt: oneDayAgo,
value: v4(),
},
{
id: id3,
env: { type: 'web' } as any,
applicationId: v4(),
userId: v4(),
playerId: v4(),
entity: 'mobile',
entityId: 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(),
entity: 'mobile',
entityId: 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(),
entity: 'mobile',
entityId: 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.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']}`);
});
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(),
entity: 'mobile',
entityId: 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}`);
});
// ==================== 数学运算符测试 ====================
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}`);
});
// ==================== 字符串操作测试 ====================
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',
},
{
id: id2,
name: 'admin_role',
nickname: 'admin',
},
]
}, 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',
},
{
id: id2,
name: 'guest_role',
nickname: 'guest',
},
]
}, 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',
}
}, 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}`);
});
// ==================== 比较运算符测试 ====================
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}`);
});
// ==================== 布尔运算符测试 ====================
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}`);
});
// ==================== 日期函数测试 ====================
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(),
entity: 'mobile',
entityId: 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(),
entity: 'mobile',
entityId: 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(),
entity: 'mobile',
entityId: 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(),
entity: 'mobile',
entityId: 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(),
entity: 'mobile',
entityId: 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`);
// 测试向下取整到年 - 应该是 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(),
entity: 'mobile',
entityId: 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('[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',
config: { App: {} },
folder: '/test',
platformId: 'p1',
application$system: [
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1', description: 't1', type: 'web', config: { type: 'web', passport: [] } as any } },
{ id: v4(), action: 'create', data: { id: v4(), name: 'app2', description: 't2', type: 'web', config: { type: 'web', passport: [] } as any } },
{ id: v4(), action: 'create', data: { id: v4(), name: 'app3', description: 't3', type: 'wechatMp', config: { type: 'web', passport: [] } as any } },
]
}
} 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']}`);
});
// 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`);
// });
// ==================== 复合表达式测试 ====================
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(),
entity: 'mobile',
entityId: v4(),
refreshedAt: twoDaysAgo,
value: v4(),
},
{
id: id2,
env: { type: 'web' } as any,
applicationId: v4(),
userId: v4(),
playerId: v4(),
entity: 'mobile',
entityId: 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',
config: { App: {} },
folder: '/test',
platformId: 'p1',
application$system: [
{
id: v4(),
action: 'create',
data: {
id: appId1,
name: 'parent_app1',
description: 't1',
type: 'web',
config: { type: 'web', passport: [] } as any
}
},
{
id: v4(),
action: 'create',
data: {
id: appId2,
name: 'child_app',
description: 't2',
type: 'web',
config: { type: 'web', passport: [] } as any
}
}
]
}
} as EntityDict['system']['CreateSingle'], context, {});
await context.commit();
// TODO: 这里有问题
// // 查询: 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 && r1[0].id === appId1, `Cross-entity expression failed`);
});
// ==================== 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(),
},
{
id: id2,
action: 'test2',
data: { name: 'zz' }, // 没有 age 字段
targetEntity: 'test',
bornAt: Date.now(),
}
]
}, 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(),
},
{
id: id2,
action: 'test2',
data: { name: 'bob', role: 'user', level: 5 },
targetEntity: 'test',
bornAt: Date.now(),
},
{
id: id3,
action: 'test3',
data: { name: 'charlie', role: 'admin', level: 3 },
targetEntity: 'test',
bornAt: Date.now(),
}
]
}, 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(),
},
{
id: id2,
action: 'test2',
data: { scores: [70, 75, 80] },
targetEntity: 'test',
bornAt: Date.now(),
}
]
}, 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`);
});
// ==================== $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' },
{ id: id2, name: 'user2', nickname: 'n2' },
{ id: id3, name: 'user3', nickname: 'n3' },
]
}, 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`);
});
// ==================== 字符串 $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' },
{ id: id2, name: 'test_user', nickname: 'test' },
{ id: id3, name: 'admin_role', nickname: 'admin1' },
]
}, 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`);
});
// ==================== 投影中的表达式测试 ====================
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`);
});
// ==================== 子查询中的表达式测试 ====================
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',
config: { App: {} },
folder: '/a',
platformId: 'p1',
application$system: [
{ id: v4(), action: 'create', data: { id: v4(), name: 'app_a1', description: 't', type: 'web', config: { type: 'web', passport: [] } as any } },
{ id: v4(), action: 'create', data: { id: v4(), name: 'app_a2', description: 't', type: 'web', config: { type: 'web', passport: [] } as any } },
]
},
{
id: systemId2,
name: 'system_b',
description: 'test',
config: { App: {} },
folder: '/b',
platformId: 'p1',
application$system: [
{ id: v4(), action: 'create', data: { id: v4(), name: 'other_b1', description: 't', type: 'web', config: { type: 'web', passport: [] } as any } },
]
}
]
} 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`);
});
// ==================== 排序中使用表达式 ====================
it('[13.1]sorter with 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('house', {
id: v4(),
action: 'create',
data: [
{ id: id1, areaId: v4(), ownerId: v4(), district: '杭州', size: 150.0 },
{ id: id2, areaId: v4(), ownerId: v4(), district: '上海', size: 100.0 },
{ id: id3, areaId: v4(), ownerId: v4(), district: '北京', size: 200.0 },
]
}, context, {});
await context.commit();
// 按 size 降序排列
const r1 = await context.select('house', {
data: { id: 1, size: 1 },
filter: { id: { $in: [id1, id2, id3] } },
sorter: [
{ $attr: { size: 1 }, $direction: 'desc' }
]
}, {});
assert(r1.length === 3, `Sorter failed`);
assert(r1[0].id === id3, `Expected first to be id3 (size 200)`);
assert(r1[1].id === id1, `Expected second to be id1 (size 150)`);
assert(r1[2].id === id2, `Expected third to be id2 (size 100)`);
});
// ==================== 分页测试 ====================
it('[14.1]pagination with indexFrom and count', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
const ids: string[] = [];
const data = [] as any[];
for (let i = 0; i < 10; i++) {
const id = v4();
ids.push(id);
data.push({
id,
areaId: v4(),
ownerId: v4(),
district: `district_${i}`,
size: (i + 1) * 10.0,
});
}
await store.operate('house', {
id: v4(),
action: 'create',
data
}, context, {});
await context.commit();
// 获取第 3-5 条记录 (indexFrom=2, count=3)
const r1 = await context.select('house', {
data: { id: 1, size: 1 },
filter: { id: { $in: ids } },
sorter: [{ $attr: { size: 1 }, $direction: 'asc' }],
indexFrom: 2,
count: 3,
}, {});
assert(r1.length === 3, `Pagination failed, expected 3, got ${r1.length}`);
assert(r1[0].size === 30, `Expected first size 30, got ${r1[0].size}`);
assert(r1[1].size === 40, `Expected second size 40, got ${r1[1].size}`);
assert(r1[2].size === 50, `Expected third size 50, got ${r1[2].size}`);
// 获取最后 2 条
const r2 = await context.select('house', {
data: { id: 1, size: 1 },
filter: { id: { $in: ids } },
sorter: [{ $attr: { size: 1 }, $direction: 'asc' }],
indexFrom: 8,
count: 5, // 超出范围
}, {});
assert(r2.length === 2, `Pagination overflow failed, expected 2, got ${r2.length}`);
});
// ==================== FOR UPDATE 锁测试 ====================
it('[15.1]select for update', 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();
await context.begin();
// FOR UPDATE 查询
const r1 = await store.select('house', {
data: { id: 1, size: 1 },
filter: { id: id1 }
}, context, { forUpdate: true });
assert(r1.length === 1, `FOR UPDATE select failed`);
await context.commit();
});
// ==================== includedDeleted 选项测试 ====================
it('[16.1]select with includedDeleted option', 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();
// 删除记录
await context.begin();
await store.operate('house', {
id: v4(),
action: 'remove',
data: {},
filter: { id: id1 }
}, context, {});
await context.commit();
// 正常查询应该查不到
const r1 = await context.select('house', {
data: { id: 1 },
filter: { id: id1 }
}, {});
assert(r1.length === 0, `Deleted record should not be visible`);
// 使用 includedDeleted 应该能查到
const r2 = await store.select('house', {
data: { id: 1 },
filter: { id: id1 }
}, context, { includedDeleted: true });
assert(r2.length === 1, `includedDeleted should show deleted record`);
});
// ==================== 物理删除测试 ====================
it('[16.2]delete physically', 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();
// TODO: 暂不支持物理删除测试
// 物理删除
await context.begin();
await store.operate('house', {
id: v4(),
action: 'remove',
data: {},
filter: { id: id1 }
}, context, { deletePhysically: true });
await context.commit();
// 即使使用 includedDeleted 也查不到
const r1 = await store.select('house', {
data: { id: 1 },
filter: { id: id1 }
}, context, { includedDeleted: true });
assert(r1.length === 0, `Physically deleted record should not exist`);
});
// ==================== 跨表 JOIN 更新测试 ====================
// TODO: 下面会出现死锁,暂不知道为什么
it('[17.1]update with join filter', 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: 'join_test_system',
description: 'test',
config: { App: {} },
folder: '/test',
platformId: 'p1',
application$system: [
{ id: v4(), action: 'create', data: { id: appId1, name: 'join_app1', description: 'old', type: 'web', config: { type: 'web', passport: [] } as any } },
{ id: v4(), action: 'create', data: { id: appId2, name: 'join_app2', description: 'old', type: 'wechatMp', config: { type: 'web', passport: [] } as any } },
]
}
} as EntityDict['system']['CreateSingle'], context, {});
await context.commit();
// 通过 system 过滤更新 application
await context.begin();
await store.operate('application', {
id: v4(),
action: 'update',
data: { description: 'updated' },
filter: {
system: {
name: 'join_test_system'
},
type: 'web'
}
}, context, {});
await context.commit();
// 验证只更新了 type='web' 的记录
const r1 = await context.select('application', {
data: { id: 1, description: 1 },
filter: { id: appId1 }
}, {});
assert(r1.length === 1 && r1[0].description === 'updated', `Join update failed for appId1`);
const r2 = await context.select('application', {
data: { id: 1, description: 1 },
filter: { id: appId2 }
}, {});
assert(r2.length === 1 && r2[0].description === 'old', `Join update should not affect appId2`);
});
// ==================== 空值处理测试 ====================
// TODO: 空值会报错
it('[18.1]null value handling', 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: 'user_with_ref', nickname: 'u1', refId: v4() },
{ id: id2, name: 'user_without_ref', nickname: 'u2' }, // refId 为 null
]
}, context, {});
await context.commit();
// 查询 refId 为 null 的记录
const r1 = await context.select('user', {
data: { id: 1, name: 1 },
filter: {
id: { $in: [id1, id2] },
refId: null as any
}
}, {});
assert(r1.length === 1 && r1[0].id === id2, `Null filter failed`);
// 查询 refId 不为 null 的记录
const r2 = await context.select('user', {
data: { id: 1, name: 1 },
filter: {
id: { $in: [id1, id2] },
refId: { $ne: null as any }
}
}, {});
assert(r2.length === 1 && r2[0].id === id1, `Not null filter failed`);
});
// ==================== 枚举类型测试 ====================
it('[19.1]enum type insert and filter', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
const id1 = v4();
const id2 = v4();
await store.operate('application', {
id: v4(),
action: 'create',
data: [
{ id: id1, name: 'web_app', description: 't', type: 'web', systemId: v4(), config: { type: 'web', passport: [] } as any },
{ id: id2, name: 'mp_app', description: 't', type: 'wechatMp', systemId: v4(), config: { type: 'web', passport: [] } as any },
]
}, context, {});
await context.commit();
// 按枚举值过滤
const r1 = await context.select('application', {
data: { id: 1, type: 1 },
filter: {
id: { $in: [id1, id2] },
type: 'web'
}
}, {});
assert(r1.length === 1 && r1[0].id === id1, `Enum filter failed`);
// 枚举 $in 查询
const r2 = await context.select('application', {
data: { id: 1, type: 1 },
filter: {
id: { $in: [id1, id2] },
type: { $in: ['web', 'wechatMp'] }
}
}, {});
assert(r2.length === 2, `Enum $in filter failed`);
});
// ==================== 复杂嵌套查询测试 ====================
it('[20.1]deeply nested filter', async () => {
const store = storeGetter();
const context = new TestContext(store);
await context.begin();
const platformId = v4();
const systemId = v4();
const appId = v4();
// 创建 platform -> system -> application 层级结构
await store.operate('platform', {
id: v4(),
action: 'create',
data: {
id: platformId,
name: 'test_platform',
description: 'test',
config: {} as any,
}
}, context, {});
await store.operate('system', {
id: v4(),
action: 'create',
data: {
id: systemId,
name: 'nested_system',
description: 'test',
config: { App: {} },
folder: '/test',
platformId,
application$system: [
{ id: v4(), action: 'create', data: { id: appId, name: 'nested_app', description: 't', type: 'web', config: { type: 'web', passport: [] } as any } },
]
}
} as EntityDict['system']['CreateSingle'], context, {});
await context.commit();
// 三层嵌套查询: application -> system -> platform
const r1 = await context.select('application', {
data: {
id: 1,
name: 1,
system: {
id: 1,
name: 1,
platform: {
id: 1,
name: 1,
}
}
},
filter: {
id: appId,
system: {
platform: {
name: 'test_platform'
}
}
}
}, {});
assert(r1.length === 1, `Deeply nested query failed`);
assert(r1[0].system?.platform?.name === 'test_platform', `Nested projection 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',
config: { App: {} },
folder: '/test1',
platformId: 'p1',
application$system: [
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1_1', description: 't', type: 'web', config: { type: 'web', passport: [] } as any } },
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1_2', description: 't', type: 'web', config: { type: 'web', passport: [] } as any } },
{ id: v4(), action: 'create', data: { id: v4(), name: 'app1_3', description: 't', type: 'wechatMp', config: { type: 'web', passport: [] } as any } },
]
},
{
id: systemId2,
name: 'agg_system2',
description: 'test',
config: { App: {} },
folder: '/test2',
platformId: 'p1',
application$system: [
{ id: v4(), action: 'create', data: { id: v4(), name: 'app2_1', description: 't', type: 'web', config: { type: 'web', passport: [] } as any } },
]
}
]
} 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`);
});
}