This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
| 3 | ESM only,不打包(`exports: "./src/index.js"`) |
|
||||
| 4 | 两种输出:`FindOptionsWhere[]`(对象式)+ QueryBuilder 条件(表达式式) |
|
||||
| 5 | AND 项合并同一对象;OR 项拆数组元素(TypeORM 语义) |
|
||||
| 6 | 嵌套分组(Q6)用括号表达式递归生成 |
|
||||
| 6 | **深层 OR 嵌套(v0.1.1)**:`toFindOptionsWhere` 接受 `toTree` 树输入,递归 + 分配律展开(`X AND (A OR B)` → `[{X,A},{X,B}]`),同字段 AND 用 `And()` 组合 |
|
||||
| 7 | 参数化绑定(`:p1 :p2`),杜绝字符串拼接注入 |
|
||||
| 8 | 空 value 且非 NULL 查询自动跳过;LIKE 自动补 `%` |
|
||||
| 9 | `disabled`/`hidden` 默认跳过(对齐 Q60/Q61),`skipEmpty` 可配 |
|
||||
@@ -40,5 +40,5 @@ EQ NE GT GTE LT LTE LIKE IN NOT_IN BETWEEN IS_NULL IS_NOT_NULL
|
||||
|
||||
## 未决项 / 后续
|
||||
|
||||
- TypeORM `FindOptionsWhere` 对深层 OR 嵌套(如 `A OR (B AND C)` 内嵌)表达能力有限,深层场景建议走 `applyToQueryBuilder`
|
||||
- 可选:`trans-form-knex`(决策 #24 提及的另一适配方向)
|
||||
- 分配律展开的最坏情况指数增长:极端复杂布尔树走 `applyToQueryBuilder`(括号表达式,无展开成本)
|
||||
|
||||
@@ -40,11 +40,25 @@ const { qb, parameters } = applyToQueryBuilder(repo.createQueryBuilder('u'), fil
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `toFindOptionsWhere(filters, options?)` | → `FindOptionsWhere[]`。AND 项合并进同一对象,OR 项拆成数组元素(`where: [{a:1},{b:2}]` = a=1 OR b=2) |
|
||||
| `toFindOptionsWhere(filters, options?)` | → `FindOptionsWhere[]`。支持**深层 OR 嵌套**(递归 + 分配律展开):AND 项合并进同一对象,OR 项拆成数组元素,`X AND (A OR B)` 自动展开为 `[{X,A},{X,B}]` |
|
||||
| `applyToQueryBuilder(qb, filters, options?)` | → `{ qb, parameters }`。支持嵌套分组括号表达式、参数绑定、alias 前缀 |
|
||||
| `mapOperator(query, operatorMap?)` | 操作符码 → 工厂函数 |
|
||||
| `DEFAULT_OPERATOR_MAP` | 默认操作符映射表 |
|
||||
|
||||
### 深层 OR 嵌套(toFindOptionsWhere)
|
||||
|
||||
输入支持 `toTree` 输出的嵌套结构(`{ logic, children }`),自动展开:
|
||||
|
||||
| 逻辑 | 输出 |
|
||||
|------|------|
|
||||
| `A OR B` | `[{A}, {B}]` |
|
||||
| `X AND (A OR B)` | `[{X,A}, {X,B}]`(分配律) |
|
||||
| `(A OR B) AND (C OR D)` | `[{A,C},{A,D},{B,C},{B,D}]`(笛卡尔积) |
|
||||
| `A OR (B AND C)` | `[{A}, {B,C}]` |
|
||||
| 同字段 AND(`name='a' AND name LIKE 'b'`) | `And(Equal('a'), Like('b'))` |
|
||||
|
||||
> 注意:分配律展开在最坏情况下呈指数增长(每组 OR 分支相乘)。实际查询条件数量有限,可放心使用;极端复杂的布尔树建议走 `applyToQueryBuilder`(括号表达式,无展开成本)。
|
||||
|
||||
### 选项
|
||||
|
||||
**toFindOptionsWhere**:`{ skipFlags = ['disabled','hidden'], skipEmpty = false, operatorMap }`
|
||||
|
||||
Generated
+388
-4
@@ -8,6 +8,9 @@
|
||||
"name": "@windychen/trans-form-typeorm",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typeorm": "^0.3.20",
|
||||
"vitest": "^3.2.4"
|
||||
@@ -1091,7 +1094,6 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "http://verdaccio:4873/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1108,6 +1110,64 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.11.1",
|
||||
"resolved": "http://verdaccio:4873/better-sqlite3/-/better-sqlite3-12.11.1.tgz",
|
||||
"integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "http://verdaccio:4873/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "http://verdaccio:4873/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "http://verdaccio:4873/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "http://verdaccio:4873/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
@@ -1230,6 +1290,12 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "http://verdaccio:4873/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "http://verdaccio:4873/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -1384,6 +1450,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "http://verdaccio:4873/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "http://verdaccio:4873/dedent/-/dedent-1.7.2.tgz",
|
||||
@@ -1409,6 +1490,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "http://verdaccio:4873/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "http://verdaccio:4873/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
@@ -1427,6 +1517,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "http://verdaccio:4873/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "http://verdaccio:4873/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -1469,6 +1568,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "http://verdaccio:4873/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "http://verdaccio:4873/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -1571,6 +1679,15 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "http://verdaccio:4873/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "http://verdaccio:4873/expect-type/-/expect-type-1.4.0.tgz",
|
||||
@@ -1599,6 +1716,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "http://verdaccio:4873/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "http://verdaccio:4873/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -1632,6 +1755,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "http://verdaccio:4873/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "http://verdaccio:4873/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1706,6 +1835,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "http://verdaccio:4873/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "http://verdaccio:4873/glob/-/glob-10.5.0.tgz",
|
||||
@@ -1800,7 +1935,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "http://verdaccio:4873/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1821,7 +1955,12 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "http://verdaccio:4873/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "http://verdaccio:4873/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-callable": {
|
||||
@@ -1934,6 +2073,18 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "http://verdaccio:4873/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "http://verdaccio:4873/minimatch/-/minimatch-9.0.9.tgz",
|
||||
@@ -1950,6 +2101,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "http://verdaccio:4873/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "http://verdaccio:4873/minipass/-/minipass-7.1.3.tgz",
|
||||
@@ -1960,6 +2120,12 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "http://verdaccio:4873/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "http://verdaccio:4873/ms/-/ms-2.1.3.tgz",
|
||||
@@ -1986,6 +2152,33 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "http://verdaccio:4873/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.94.0",
|
||||
"resolved": "http://verdaccio:4873/node-abi/-/node-abi-3.94.0.tgz",
|
||||
"integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "http://verdaccio:4873/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "http://verdaccio:4873/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -2096,6 +2289,71 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "http://verdaccio:4873/prebuild-install/-/prebuild-install-7.1.2.tgz",
|
||||
"integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^1.0.1",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "http://verdaccio:4873/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "http://verdaccio:4873/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "http://verdaccio:4873/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "http://verdaccio:4873/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
@@ -2163,7 +2421,6 @@
|
||||
"version": "5.2.1",
|
||||
"resolved": "http://verdaccio:4873/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2180,6 +2437,18 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "http://verdaccio:4873/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "http://verdaccio:4873/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -2262,6 +2531,51 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "http://verdaccio:4873/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "http://verdaccio:4873/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "http://verdaccio:4873/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2303,6 +2617,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "http://verdaccio:4873/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "http://verdaccio:4873/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -2407,6 +2730,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "http://verdaccio:4873/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "http://verdaccio:4873/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
@@ -2420,6 +2752,34 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "http://verdaccio:4873/tar-fs/-/tar-fs-2.1.5.tgz",
|
||||
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "http://verdaccio:4873/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "http://verdaccio:4873/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -2503,6 +2863,18 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "http://verdaccio:4873/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-array-buffer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "http://verdaccio:4873/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||
@@ -2621,6 +2993,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "http://verdaccio:4873/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "http://verdaccio:4873/uuid/-/uuid-11.1.1.tgz",
|
||||
@@ -2959,6 +3337,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "http://verdaccio:4873/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "http://verdaccio:4873/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windychen/trans-form-typeorm",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "TypeORM adapter for @windychen/trans-form-middleware: structured filters to FindOptionsWhere / QueryBuilder",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -27,5 +27,8 @@
|
||||
"devDependencies": {
|
||||
"typeorm": "^0.3.20",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
+147
-59
@@ -1,3 +1,4 @@
|
||||
import { And } from 'typeorm';
|
||||
import { mapOperator } from './operatorMap.js';
|
||||
|
||||
const NULL_QUERIES = new Set(['IS_NULL', 'IS_NOT_NULL']);
|
||||
@@ -14,7 +15,6 @@ function isEmptyValue(value) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip when value is empty, there are no `values`, and query is not a null-check.
|
||||
* @param {object} filter
|
||||
* @returns {boolean}
|
||||
*/
|
||||
@@ -27,26 +27,151 @@ function shouldSkipEmptyValue(filter) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert parsed filters into TypeORM `FindOptionsWhere[]`.
|
||||
* Merge a leaf entry into a group object, combining same-field keys with And().
|
||||
* @param {Record<string, *>} target
|
||||
* @param {Record<string, *>} entry
|
||||
* @returns {Record<string, *>}
|
||||
*/
|
||||
function mergeEntry(target, entry) {
|
||||
for (const [key, value] of Object.entries(entry)) {
|
||||
if (key in target) {
|
||||
target[key] = And(target[key], value);
|
||||
} else {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cartesian product: merge every element of `groups` with every element of `converted`.
|
||||
* Used to distribute an outer AND group over an inner OR group:
|
||||
* X AND (A OR B) => [{X,A}, {X,B}]
|
||||
* @param {Record<string, *>[]} groups
|
||||
* @param {Record<string, *>[]} converted
|
||||
* @returns {Record<string, *>[]}
|
||||
*/
|
||||
function cartesian(groups, converted) {
|
||||
const out = [];
|
||||
for (const g of groups) {
|
||||
for (const el of converted) {
|
||||
out.push(mergeEntry({ ...g }, el));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively build a TypeORM FindOptionsWhere object or array from a group
|
||||
* of filters (flat list or nested `{ logic, children }` tree).
|
||||
*
|
||||
* AND items merge into the same object; OR items become separate array elements
|
||||
* (`where: [{a:1},{b:2}]` ⇒ `a=1 OR b=2`). Pure AND returns `[{...}]`;
|
||||
* empty input returns `[]`.
|
||||
* Semantics:
|
||||
* - AND items merge into the same object (`{a:1, b:2}`)
|
||||
* - OR items become separate array elements (`[{a:1},{b:2}]`)
|
||||
* - an inner OR group appearing after outer AND items is distributed
|
||||
* via the cartesian product (`X AND (A OR B)` => `[{X,A},{X,B}]`)
|
||||
* - same-field AND is combined with TypeORM `And()`
|
||||
*
|
||||
* The first accepted filter always starts an AND group (even if it carries `logic`).
|
||||
* @param {Array<object>} items
|
||||
* @param {{
|
||||
* skipFlags?: string[],
|
||||
* skipEmpty?: boolean,
|
||||
* operatorMap?: Record<string, *>,
|
||||
* }} options
|
||||
* @returns {Record<string, *> | Record<string, *>[]}
|
||||
*/
|
||||
function buildGroup(items, options) {
|
||||
const { skipFlags = ['disabled', 'hidden'], skipEmpty = false, operatorMap } = options;
|
||||
|
||||
/** @type {Record<string, *>} */
|
||||
let obj = {};
|
||||
/** @type {Record<string, *>[]} */
|
||||
let parts = [];
|
||||
let inOrMode = false;
|
||||
|
||||
const flushObj = () => {
|
||||
if (Object.keys(obj).length > 0) {
|
||||
parts.push(obj);
|
||||
obj = {};
|
||||
inOrMode = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
if (skipFlags.some((flag) => item[flag] === true)) continue;
|
||||
if (skipEmpty && item.empty === true) continue;
|
||||
|
||||
// Nested group node (toTree output): { logic, children }
|
||||
let converted;
|
||||
if (Array.isArray(item.children)) {
|
||||
const childItems = item.children.filter((c) => {
|
||||
if (!c || typeof c !== 'object') return false;
|
||||
if (skipFlags.some((flag) => c[flag] === true)) return false;
|
||||
if (skipEmpty && c.empty === true) return false;
|
||||
if (c.field == null && !Array.isArray(c.children)) return false;
|
||||
return true;
|
||||
});
|
||||
if (!childItems.length) continue;
|
||||
converted = buildGroup(childItems, options);
|
||||
} else {
|
||||
if (shouldSkipEmptyValue(item)) continue;
|
||||
if (item.field == null || item.query == null) continue;
|
||||
const factory = mapOperator(item.query, operatorMap);
|
||||
const findOperator = factory(item.value, item.values);
|
||||
converted = { [String(item.field)]: findOperator };
|
||||
}
|
||||
|
||||
const logic = String(item.logic || 'AND').toUpperCase();
|
||||
if (logic === 'OR' && Object.keys(obj).length > 0) {
|
||||
flushObj();
|
||||
}
|
||||
|
||||
if (Array.isArray(converted)) {
|
||||
// Inner OR group — distribute over accumulated parts / obj.
|
||||
if (parts.length > 0) {
|
||||
parts = cartesian(parts, converted);
|
||||
} else if (Object.keys(obj).length > 0) {
|
||||
parts = converted.map((el) => mergeEntry({ ...obj }, el));
|
||||
obj = {};
|
||||
} else {
|
||||
parts = converted;
|
||||
}
|
||||
inOrMode = true;
|
||||
} else if (logic === 'OR') {
|
||||
// New OR branch — push as a separate array element.
|
||||
parts.push(converted);
|
||||
inOrMode = true;
|
||||
} else if (parts.length > 0 && inOrMode) {
|
||||
// AND connection in OR mode — distribute into every element.
|
||||
parts = parts.map((p) => mergeEntry({ ...p }, converted));
|
||||
} else {
|
||||
// Plain AND — merge into the current object.
|
||||
mergeEntry(obj, converted);
|
||||
}
|
||||
}
|
||||
|
||||
flushObj();
|
||||
|
||||
// A single-element array only happens when the whole group was plain AND
|
||||
// (flushObj pushed one object). Unwrap it so the caller does not treat it
|
||||
// as an OR group and run the cartesian product over it.
|
||||
if (inOrMode) {
|
||||
return parts.length === 1 ? parts[0] : parts;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert parsed filters (flat `parseFilters` output or nested `toTree` output)
|
||||
* into TypeORM `FindOptionsWhere[]`.
|
||||
*
|
||||
* @param {Array<{
|
||||
* field: string,
|
||||
* query: string,
|
||||
* op?: string,
|
||||
* value?: *,
|
||||
* values?: *[],
|
||||
* logic?: string,
|
||||
* empty?: boolean,
|
||||
* disabled?: boolean,
|
||||
* hidden?: boolean,
|
||||
* readOnly?: boolean,
|
||||
* }>} filters - `@windychen/trans-form-middleware` `parseFilters` output
|
||||
* Supports deep OR nesting via recursive expansion:
|
||||
* - `A OR B` => `[{A}, {B}]`
|
||||
* - `X AND (A OR B)` => `[{X,A}, {X,B}]` (distributive law)
|
||||
* - `(A OR B) AND (C OR D)` => `[{A,C},{A,D},{B,C},{B,D}]`
|
||||
*
|
||||
* @param {Array<object>} filters
|
||||
* @param {{
|
||||
* skipFlags?: string[],
|
||||
* skipEmpty?: boolean,
|
||||
@@ -55,47 +180,10 @@ function shouldSkipEmptyValue(filter) {
|
||||
* @returns {import('typeorm').FindOptionsWhere<*>[]}
|
||||
*/
|
||||
export function toFindOptionsWhere(filters, options = {}) {
|
||||
const {
|
||||
skipFlags = ['disabled', 'hidden'],
|
||||
skipEmpty = false,
|
||||
operatorMap,
|
||||
} = options;
|
||||
|
||||
if (!Array.isArray(filters) || filters.length === 0) return [];
|
||||
|
||||
/** @type {Record<string, *>[]} */
|
||||
const groups = [];
|
||||
/** @type {Record<string, *>} */
|
||||
let current = {};
|
||||
|
||||
const flush = () => {
|
||||
if (Object.keys(current).length > 0) {
|
||||
groups.push(current);
|
||||
current = {};
|
||||
}
|
||||
};
|
||||
|
||||
for (const filter of filters) {
|
||||
if (!filter || typeof filter !== 'object') continue;
|
||||
if (skipFlags.some((flag) => filter[flag] === true)) continue;
|
||||
if (skipEmpty && filter.empty === true) continue;
|
||||
if (shouldSkipEmptyValue(filter)) continue;
|
||||
if (filter.field == null || filter.query == null) continue;
|
||||
|
||||
const factory = mapOperator(filter.query, operatorMap);
|
||||
const findOperator = factory(filter.value, filter.values);
|
||||
const entry = { [String(filter.field)]: findOperator };
|
||||
|
||||
// First group starts as AND regardless of `logic` on the first item.
|
||||
const logic = String(filter.logic || 'AND').toUpperCase();
|
||||
if (logic === 'OR' && Object.keys(current).length > 0) {
|
||||
flush();
|
||||
}
|
||||
|
||||
Object.assign(current, entry);
|
||||
}
|
||||
|
||||
flush();
|
||||
return groups;
|
||||
const built = buildGroup(filters, options);
|
||||
if (Array.isArray(built)) return built;
|
||||
if (Object.keys(built).length === 0) return [];
|
||||
return [built];
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,120 @@ describe('toFindOptionsWhere', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('toFindOptionsWhere 深层 OR 嵌套', () => {
|
||||
it('X AND (A OR B) 分配律展开为 [{X,A},{X,B}]', () => {
|
||||
const out = toFindOptionsWhere([
|
||||
{ field: 'x', query: 'EQ', value: 1 },
|
||||
{
|
||||
logic: 'AND',
|
||||
children: [
|
||||
{ field: 'a', query: 'EQ', value: 2 },
|
||||
{ field: 'b', query: 'EQ', value: 3, logic: 'OR' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(out).toHaveLength(2);
|
||||
expect(out[0].x.value).toBe(1);
|
||||
expect(out[0].a.value).toBe(2);
|
||||
expect(out[1].x.value).toBe(1);
|
||||
expect(out[1].b.value).toBe(3);
|
||||
});
|
||||
|
||||
it('(A OR B) AND (C OR D) 笛卡尔积展开', () => {
|
||||
const out = toFindOptionsWhere([
|
||||
{
|
||||
logic: 'AND',
|
||||
children: [
|
||||
{ field: 'a', query: 'EQ', value: 1 },
|
||||
{ field: 'b', query: 'EQ', value: 2, logic: 'OR' },
|
||||
],
|
||||
},
|
||||
{
|
||||
logic: 'AND',
|
||||
children: [
|
||||
{ field: 'c', query: 'EQ', value: 3 },
|
||||
{ field: 'd', query: 'EQ', value: 4, logic: 'OR' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(out).toHaveLength(4);
|
||||
// [{A,C},{A,D},{B,C},{B,D}]
|
||||
const combos = out.map((g) => Object.keys(g).sort().join(','));
|
||||
expect(combos).toEqual(expect.arrayContaining(['a,c', 'a,d', 'b,c', 'b,d']));
|
||||
});
|
||||
|
||||
it('A OR (B AND C) 保持两组', () => {
|
||||
const out = toFindOptionsWhere([
|
||||
{ field: 'a', query: 'EQ', value: 1 },
|
||||
{
|
||||
logic: 'OR',
|
||||
children: [
|
||||
{ field: 'b', query: 'EQ', value: 2 },
|
||||
{ field: 'c', query: 'EQ', value: 3, logic: 'AND' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(out).toHaveLength(2);
|
||||
expect(out[0]).toEqual({ a: Equal(1) });
|
||||
expect(out[1].b.value).toBe(2);
|
||||
expect(out[1].c.value).toBe(3);
|
||||
});
|
||||
|
||||
it('同字段 AND 用 And() 组合', () => {
|
||||
const out = toFindOptionsWhere([
|
||||
{ field: 'name', query: 'EQ', value: 'a' },
|
||||
{ field: 'name', query: 'LIKE', value: 'b', logic: 'AND' },
|
||||
]);
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0].name.type).toBe('and');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toFindOptionsWhere 真实 SQL 验证', () => {
|
||||
it('深层嵌套生成正确 SQL', async () => {
|
||||
const { DataSource, EntitySchema } = await import('typeorm');
|
||||
const User = new EntitySchema({
|
||||
name: 'User',
|
||||
tableName: 'users',
|
||||
columns: {
|
||||
id: { type: 'int', primary: true, generated: true },
|
||||
name: { type: 'varchar' },
|
||||
age: { type: 'int' },
|
||||
},
|
||||
});
|
||||
const ds = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: ':memory:',
|
||||
entities: [User],
|
||||
synchronize: true,
|
||||
});
|
||||
await ds.initialize();
|
||||
try {
|
||||
const repo = ds.getRepository('User');
|
||||
const where = toFindOptionsWhere([
|
||||
{ field: 'name', query: 'EQ', value: 'a' },
|
||||
{
|
||||
logic: 'AND',
|
||||
children: [
|
||||
{ field: 'age', query: 'GT', value: 18 },
|
||||
{ field: 'age', query: 'LT', value: 60, logic: 'OR' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const qb = repo.createQueryBuilder('u');
|
||||
qb.where(where);
|
||||
const [sql, params] = qb.getQueryAndParameters();
|
||||
// 分配律: X AND (A OR B) => (X AND A) OR (X AND B)
|
||||
expect(sql).toContain('("u"."name" = ? AND "u"."age" > 18)');
|
||||
expect(sql).toContain('("u"."name" = ? AND "u"."age" < 60)');
|
||||
expect(sql).toContain(' OR ');
|
||||
expect(params.filter((p) => p === 'a')).toHaveLength(2);
|
||||
} finally {
|
||||
await ds.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyToQueryBuilder', () => {
|
||||
it('单 EQ → andWhere("name = :p1", {p1:"xx"})', () => {
|
||||
const qb = createMockQb();
|
||||
|
||||
Reference in New Issue
Block a user