From aa49a12cb0a5fe7ab7d781faa5bd5b295dee4da6 Mon Sep 17 00:00:00 2001 From: Pan Qiancheng <905739777@qq.com> Date: Thu, 1 Jan 2026 11:23:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- test/entities/House.ts | 2 +- test/testMySQLStore.ts | 2138 +--------------- test/testPostgresStore.ts | 2177 +--------------- test/testSqlTranslator.ts | 2 +- test/testcase/index.ts | 5117 +++++++++++++++++++++++++++++++++++++ test/utils/test.ts | 108 + tsconfig.test.json | 32 + 8 files changed, 5275 insertions(+), 4306 deletions(-) create mode 100644 test/testcase/index.ts create mode 100644 test/utils/test.ts create mode 100644 tsconfig.test.json diff --git a/package.json b/package.json index b6dc27e..3518167 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "scripts": { "test": "mocha", "make:test:domain": "ts-node script/makeTestDomain.ts", - "build": "tsc" + "build": "tsc", + "build:test": "tsc -p tsconfig.test.json", + "test:mysql": "node ./test-dist/testMySQLStore.js", + "test:postgres": "node ./test-dist/testPostgresStore.js" }, "dependencies": { "lodash": "^4.17.21", diff --git a/test/entities/House.ts b/test/entities/House.ts index cd61af7..448564e 100644 --- a/test/entities/House.ts +++ b/test/entities/House.ts @@ -10,7 +10,7 @@ export interface Schema extends EntityShape { area: Area; owner: User; dd: Array; - size: Float<4, 2>; + size: Float<8, 4>; }; const locale: LocaleDef = { diff --git a/test/testMySQLStore.ts b/test/testMySQLStore.ts index 65c21af..44de369 100644 --- a/test/testMySQLStore.ts +++ b/test/testMySQLStore.ts @@ -1,15 +1,14 @@ import assert from 'assert'; -import { describe, it } from 'mocha'; import { TestContext } from './Context'; import { v4 } from 'uuid'; -import { MysqlStore } from '../src/MySQL/store'; +import { MysqlStore } from '../lib/MySQL/store'; import { EntityDict, storageSchema } from './test-app-domain'; -import { filter } from 'lodash'; 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 { tests } from './testcase'; -describe('test mysqlstore', function () { - this.timeout(100000); +describe('test mysqlstore', async function () { let store: MysqlStore; before(async () => { @@ -27,2134 +26,7 @@ describe('test mysqlstore', function () { }); }); - it('test insert', async () => { - 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 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 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 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 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 () => { - // 这个例子暂在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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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)); - }); + tests(() => store!) after(() => { store.disconnect(); diff --git a/test/testPostgresStore.ts b/test/testPostgresStore.ts index 5b81f68..27d1e9a 100644 --- a/test/testPostgresStore.ts +++ b/test/testPostgresStore.ts @@ -1,58 +1,24 @@ -import { PostgreSQLStore } from './../PostgreSQL/store'; +import { PostgreSQLStore } from '../lib/PostgreSQL/store'; import assert from 'assert'; import { TestContext } from './Context'; import { v4 } from 'uuid'; import { EntityDict, storageSchema } from './test-app-domain'; -import { filter } from 'lodash'; import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import { WebConfig } from './test-app-domain/Application/Schema'; - -const beforeFuncs : Array<() => Promise> = []; -const afterFuncs: Array<() => void> = []; - -const itFuncs: Array<() => Promise> = []; - -const before = (func: () => Promise) => { - beforeFuncs.push(func); - return func; -} - -const it = (desc: string, func: () => Promise) => { - itFuncs.push(async () => { - console.log(' ' + desc); - await func(); - }); -} - -const after = (func: () => void) => { - afterFuncs.push(func); - return func; -} - -const describe = async (desc: string, func: () => Promise) => { - console.log(desc); - func(); - for (const f of beforeFuncs) { - await f(); - } - for (const f of itFuncs) { - await f(); - } - for (const f of afterFuncs) { - f(); - } -}; +import { describe, it, before, after } from './utils/test'; +import { tests } from './testcase'; describe('test postgresstore', async function () { let store: PostgreSQLStore; before(async () => { store = new PostgreSQLStore(storageSchema, { - host: '192.168.34.15', + host: 'localhost', database: 'oakdb', user: 'postgres', password: 'postgres', max: 20, + port: 5432, }); store.connect(); await store.initialize({ @@ -60,2137 +26,8 @@ describe('test postgresstore', async function () { }); }); - it('test insert', async () => { - 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 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 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 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 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 () => { - // 这个例子暂在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 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 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 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 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 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 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 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, {}); - // 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 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 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 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 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 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 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 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(); - // TODO: 这里出现了.$or的写法,暂时不支持 - // assert(row.length === 1); - // assert(row2.length === 1); - }); - - it('[1.12]complicated json filter', async () => { - 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 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); - - // TODO: 这里所有涉及all和not all的情况似乎都有问题,以后再说 - - // 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)); - }); - + tests(() => store!) + after(() => { store.disconnect(); }); diff --git a/test/testSqlTranslator.ts b/test/testSqlTranslator.ts index 97df1b8..8e034d2 100644 --- a/test/testSqlTranslator.ts +++ b/test/testSqlTranslator.ts @@ -1,5 +1,5 @@ import { describe, it } from 'mocha'; -import { MySqlTranslator } from '../src/MySQL/translator'; +import { MySqlTranslator } from '../lib/MySQL/translator'; import { EntityDict, storageSchema } from './test-app-domain'; describe('test MysqlTranslator', function () { diff --git a/test/testcase/index.ts b/test/testcase/index.ts new file mode 100644 index 0000000..0299681 --- /dev/null +++ b/test/testcase/index.ts @@ -0,0 +1,5117 @@ +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`); + }); + +} \ No newline at end of file diff --git a/test/utils/test.ts b/test/utils/test.ts new file mode 100644 index 0000000..ace984c --- /dev/null +++ b/test/utils/test.ts @@ -0,0 +1,108 @@ +type AsyncFunc = () => Promise; +type SyncFunc = () => void; + +const beforeFuncs: AsyncFunc[] = []; +const itFuncs: { desc: string; fn: AsyncFunc }[] = []; +const afterFuncs: SyncFunc[] = []; + +const stats = { + total: 0, + passed: 0, + failed: 0, + errors: [] as { + type: 'before' | 'it' | 'after'; + desc?: string; + error: unknown; + }[], +}; + +export const before = (func: AsyncFunc) => { + beforeFuncs.push(func); + return func; +}; + +export const it = (desc: string, func: AsyncFunc) => { + itFuncs.push({ desc, fn: func }); +}; + +export const after = (func: SyncFunc) => { + afterFuncs.push(func); + return func; +}; + +export const describe = async (desc: string, func: () => Promise | void) => { + console.log(desc); + + // 注册阶段 + await func(); + + // before + for (const f of beforeFuncs) { + try { + await f(); + } catch (err) { + stats.failed++; + stats.errors.push({ + type: 'before', + error: err, + }); + console.error(' ✗ before failed:', err); + } + } + + // it + for (const { desc, fn } of itFuncs) { + stats.total++; + try { + console.log(' ' + desc); + await fn(); + stats.passed++; + console.log(' ✓ passed'); + } catch (err) { + stats.failed++; + stats.errors.push({ + type: 'it', + desc, + error: err, + }); + console.error(' ✗ failed:', err); + } + } + + // after(无论如何都执行) + for (const f of afterFuncs) { + try { + f(); + } catch (err) { + stats.failed++; + stats.errors.push({ + type: 'after', + error: err, + }); + console.error(' ✗ after failed:', err); + } + } + + // 统计输出 + printSummary(); +}; + +const printSummary = () => { + console.log('\n====== Test Summary ======'); + console.log(`Total: ${stats.total}`); + console.log(`Passed: ${stats.passed}`); + console.log(`Failed: ${stats.failed}`); + + if (stats.errors.length) { + console.log('\nErrors:'); + for (const e of stats.errors) { + console.log( + `- [${e.type}]${e.desc ? ' ' + e.desc : ''}`, + '\n ', + e.error + ); + } + } + + console.log('=========================='); +}; diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..798fde4 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ESNext", + "declaration": true, + "allowJs": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "strict": true, + "skipLibCheck": true, + "importHelpers": true, + "lib": [ + "ESNext", + "DOM" + ], + "outDir": "test-dist", /* Redirect output structure to the directory. */ + "rootDir": "test", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "types": [ + "node", + "mocha" + ], + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true + }, + "include": [ + "test/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file