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