mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
Compare commits
9 Commits
c4f5d12f3f
...
2133c97437
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2133c97437 | ||
|
|
cc07c90f1d | ||
|
|
a46eb0a7e1 | ||
|
|
2d8c5158db | ||
|
|
6e34756b9d | ||
|
|
73fda419e4 | ||
|
|
6f486e5a67 | ||
|
|
38715bbd41 | ||
|
|
ec3ea10b44 |
28
CHANGELOG.md
28
CHANGELOG.md
@ -1,3 +1,31 @@
|
||||
## [0.3.28](https://github.com/typeorm/typeorm/compare/0.3.27...0.3.28) (2025-12-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add multiSubnetFailover option for mssql ([#10804](https://github.com/typeorm/typeorm/issues/10804)) ([83e3a8a](https://github.com/typeorm/typeorm/commit/83e3a8a3db581a50495fa2d97c8fcd5d603cfd3c))
|
||||
* circular import in SapDriver.ts ([#11750](https://github.com/typeorm/typeorm/issues/11750)) ([bed7913](https://github.com/typeorm/typeorm/commit/bed79136230d4ab26cce8cf79071134c75527857))
|
||||
* **cli:** init command reading package.json from two folders up ([#11789](https://github.com/typeorm/typeorm/issues/11789)) ([dd55218](https://github.com/typeorm/typeorm/commit/dd55218648eb449937e22e1e7c88182db0048f1d))
|
||||
* **deps:** upgrade glob to fix CVE-2025-64756 ([#11784](https://github.com/typeorm/typeorm/issues/11784)) ([dc74f53](https://github.com/typeorm/typeorm/commit/dc74f5374ef5ec83d53045e4bca99cb9ff7d49d4))
|
||||
* **mongodb:** add missing `findBy` method to MongoEntityManager ([#11814](https://github.com/typeorm/typeorm/issues/11814)) ([38715bb](https://github.com/typeorm/typeorm/commit/38715bbd4169cae2910aac035cd2b05bddbaec5c))
|
||||
* **redis:** version detection logic ([#11815](https://github.com/typeorm/typeorm/issues/11815)) ([6f486e5](https://github.com/typeorm/typeorm/commit/6f486e5a67c007287949be119f233fb2b4fb7a59))
|
||||
* typesense doc sync ([#11807](https://github.com/typeorm/typeorm/issues/11807)) ([d0b5454](https://github.com/typeorm/typeorm/commit/d0b54544e9e43a5330c0485d41551128224fe4d3))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for `jsonpath` column type in PostgreSQL ([#11684](https://github.com/typeorm/typeorm/issues/11684)) ([4f05718](https://github.com/typeorm/typeorm/commit/4f05718237a6ef1a3bc623e803536db23f1f327b))
|
||||
* **cli/init:** pick dependencies versions from our own package.json ([#11705](https://github.com/typeorm/typeorm/issues/11705)) ([b930909](https://github.com/typeorm/typeorm/commit/b9309098bc00de047a96cba642ea1ed9e730b1fa))
|
||||
* entity schema support trees ([#11606](https://github.com/typeorm/typeorm/issues/11606)) ([925dee0](https://github.com/typeorm/typeorm/commit/925dee002b92f1210456dce16c18c6b436e912f3))
|
||||
* export QueryPartialEntity and QueryDeepPartialEntity types ([#11748](https://github.com/typeorm/typeorm/issues/11748)) ([ade198c](https://github.com/typeorm/typeorm/commit/ade198c77cda65e86f057f97261073f5ab2b1ed6))
|
||||
* init version in postgres driver only if not set ([#11373](https://github.com/typeorm/typeorm/issues/11373)) ([cb1284c](https://github.com/typeorm/typeorm/commit/cb1284c8c0950dcb792e95b889efe1dfafc05aea))
|
||||
* manage MongoDB SOCKS5 proxy settings ([#11731](https://github.com/typeorm/typeorm/issues/11731)) ([d7867eb](https://github.com/typeorm/typeorm/commit/d7867ebff173e6cae45e6ce82c9f8890811c4eba))
|
||||
* **mssql:** support 'vector' type for MS SQL Server ([#11732](https://github.com/typeorm/typeorm/issues/11732)) ([2681051](https://github.com/typeorm/typeorm/commit/2681051f78c5c284b340e7978f8f337e86c7e915))
|
||||
* **mysql:** add pool size options for each connection ([#11810](https://github.com/typeorm/typeorm/issues/11810)) ([67f793f](https://github.com/typeorm/typeorm/commit/67f793feaa976da717175daf152f738793b94ed2))
|
||||
* **mysql:** add support for vector columns on MariaDB and MySQL ([#11670](https://github.com/typeorm/typeorm/issues/11670)) ([cfb3d6c](https://github.com/typeorm/typeorm/commit/cfb3d6c015ad648a7ffc08a7a11ce580d108ac69))
|
||||
|
||||
|
||||
|
||||
## [0.3.27](https://github.com/typeorm/typeorm/compare/0.3.26...0.3.27) (2025-09-19)
|
||||
|
||||
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
import js from "@eslint/js"
|
||||
import { defineConfig } from "eslint/config"
|
||||
import pluginChaiFriendly from "eslint-plugin-chai-friendly"
|
||||
import { jsdoc } from "eslint-plugin-jsdoc"
|
||||
import { defineConfig, globalIgnores } from "eslint/config"
|
||||
import globals from "globals"
|
||||
import ts from "typescript-eslint"
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
ignores: [
|
||||
"build/**",
|
||||
"docs/**",
|
||||
"node_modules/**",
|
||||
"sample/playground/**",
|
||||
"temp/**",
|
||||
],
|
||||
},
|
||||
|
||||
globalIgnores([
|
||||
"build/**",
|
||||
"docs/**",
|
||||
"node_modules/**",
|
||||
"sample/playground/**",
|
||||
"temp/**",
|
||||
]),
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
@ -31,10 +29,7 @@ export default defineConfig([
|
||||
js,
|
||||
ts,
|
||||
},
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommendedTypeChecked,
|
||||
],
|
||||
extends: [js.configs.recommended, ...ts.configs.recommendedTypeChecked],
|
||||
rules: {
|
||||
// exceptions from typescript-eslint/recommended
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
@ -45,12 +40,11 @@ export default defineConfig([
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "warn",
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "warn",
|
||||
"@typescript-eslint/no-unsafe-function-type": "warn",
|
||||
"@typescript-eslint/no-unused-expressions": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_"
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-wrapper-object-types": "off",
|
||||
@ -90,14 +84,17 @@ export default defineConfig([
|
||||
"no-regex-spaces": "warn",
|
||||
},
|
||||
},
|
||||
|
||||
jsdoc({
|
||||
config: "flat/recommended-typescript",
|
||||
files: ["src/**/*.ts"],
|
||||
config: "flat/recommended-typescript",
|
||||
// Temporarily enable individual rules when they are fixed, until all current warnings are gone,
|
||||
// and then remove manual config in favor of `config: "flat/recommended-typescript-error"`
|
||||
rules: {
|
||||
"jsdoc/valid-types": "error"
|
||||
}
|
||||
"jsdoc/valid-types": "error",
|
||||
},
|
||||
}),
|
||||
{
|
||||
files: ["test/**/*.ts"],
|
||||
...pluginChaiFriendly.configs.recommendedFlat,
|
||||
},
|
||||
])
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "typeorm",
|
||||
"version": "0.3.27",
|
||||
"version": "0.3.28",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "typeorm",
|
||||
"version": "0.3.27",
|
||||
"version": "0.3.28",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sqltools/formatter": "^1.2.5",
|
||||
@ -53,6 +53,7 @@
|
||||
"chai-as-promised": "^7.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-chai-friendly": "^1.1.0",
|
||||
"eslint-plugin-jsdoc": "^61.4.1",
|
||||
"globals": "^16.5.0",
|
||||
"gulp": "^4.0.2",
|
||||
@ -5936,6 +5937,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-chai-friendly": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.1.0.tgz",
|
||||
"integrity": "sha512-+T1rClpDdXkgBAhC16vRQMI5umiWojVqkj9oUTdpma50+uByCZM/oBfxitZiOkjMRlm725mwFfz/RVgyDRvCKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsdoc": {
|
||||
"version": "61.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.4.1.tgz",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "typeorm",
|
||||
"version": "0.3.27",
|
||||
"version": "0.3.28",
|
||||
"description": "Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.",
|
||||
"homepage": "https://typeorm.io",
|
||||
"bugs": {
|
||||
@ -134,6 +134,7 @@
|
||||
"chai-as-promised": "^7.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-chai-friendly": "^1.1.0",
|
||||
"eslint-plugin-jsdoc": "^61.4.1",
|
||||
"globals": "^16.5.0",
|
||||
"gulp": "^4.0.2",
|
||||
|
||||
28
src/cache/RedisQueryResultCache.ts
vendored
28
src/cache/RedisQueryResultCache.ts
vendored
@ -300,26 +300,22 @@ export class RedisQueryResultCache implements QueryResultCache {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the Redis version based on the connected client's API characteristics
|
||||
* without creating test keys in the database
|
||||
* Detects the Redis package version by reading the installed package.json
|
||||
* and sets the appropriate API version (3 for callback-based, 5 for Promise-based).
|
||||
*/
|
||||
private detectRedisVersion(): void {
|
||||
if (this.clientType !== "redis") return
|
||||
|
||||
try {
|
||||
// Detect version by examining the client's method signatures
|
||||
// This avoids creating test keys in the database
|
||||
const setMethod = this.client.set
|
||||
if (setMethod && setMethod.length <= 3) {
|
||||
// Redis 5+ set method accepts fewer parameters (key, value, options)
|
||||
this.redisMajorVersion = 5
|
||||
} else {
|
||||
// Redis 3/4 set method requires more parameters (key, value, flag, duration, callback)
|
||||
this.redisMajorVersion = 3
|
||||
}
|
||||
} catch {
|
||||
// Default to Redis 3/4 for maximum compatibility
|
||||
const version = PlatformTools.readPackageVersion("redis")
|
||||
const major = parseInt(version.split(".")[0], 10)
|
||||
if (isNaN(major)) {
|
||||
throw new TypeORMError(`Invalid Redis version format: ${version}`)
|
||||
}
|
||||
if (major <= 4) {
|
||||
// Redis 3/4 uses callback-based API
|
||||
this.redisMajorVersion = 3
|
||||
} else {
|
||||
// Redis 5+ uses Promise-based API
|
||||
this.redisMajorVersion = 5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -806,14 +806,20 @@ export class AuroraMysqlDriver implements Driver {
|
||||
this.poolCluster.getConnection(
|
||||
"MASTER",
|
||||
(err: any, dbConnection: any) => {
|
||||
err
|
||||
? fail(err)
|
||||
: ok(this.prepareDbConnection(dbConnection))
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok(this.prepareDbConnection(dbConnection))
|
||||
}
|
||||
},
|
||||
)
|
||||
} else if (this.pool) {
|
||||
this.pool.getConnection((err: any, dbConnection: any) => {
|
||||
err ? fail(err) : ok(this.prepareDbConnection(dbConnection))
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok(this.prepareDbConnection(dbConnection))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
fail(
|
||||
@ -837,7 +843,11 @@ export class AuroraMysqlDriver implements Driver {
|
||||
this.poolCluster.getConnection(
|
||||
"SLAVE*",
|
||||
(err: any, dbConnection: any) => {
|
||||
err ? fail(err) : ok(this.prepareDbConnection(dbConnection))
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok(this.prepareDbConnection(dbConnection))
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@ -138,7 +138,7 @@ export class BetterSqlite3Driver extends AbstractSqliteDriver {
|
||||
nativeBinding = null,
|
||||
prepareDatabase,
|
||||
} = this.options
|
||||
const databaseConnection = this.sqlite(database, {
|
||||
const databaseConnection = new this.sqlite(database, {
|
||||
readonly,
|
||||
fileMustExist,
|
||||
timeout,
|
||||
@ -148,8 +148,8 @@ export class BetterSqlite3Driver extends AbstractSqliteDriver {
|
||||
// in the options, if encryption key for SQLCipher is setted.
|
||||
// Must invoke key pragma before trying to do any other interaction with the database.
|
||||
if (this.options.key) {
|
||||
databaseConnection.exec(
|
||||
`PRAGMA key = ${JSON.stringify(this.options.key)}`,
|
||||
databaseConnection.pragma(
|
||||
`key = ${JSON.stringify(this.options.key)}`,
|
||||
)
|
||||
}
|
||||
|
||||
@ -160,11 +160,11 @@ export class BetterSqlite3Driver extends AbstractSqliteDriver {
|
||||
|
||||
// we need to enable foreign keys in sqlite to make sure all foreign key related features
|
||||
// working properly. this also makes onDelete to work with sqlite.
|
||||
databaseConnection.exec(`PRAGMA foreign_keys = ON`)
|
||||
databaseConnection.pragma("foreign_keys = ON")
|
||||
|
||||
// turn on WAL mode to enhance performance
|
||||
if (this.options.enableWAL) {
|
||||
databaseConnection.exec(`PRAGMA journal_mode = WAL`)
|
||||
databaseConnection.pragma("journal_mode = WAL")
|
||||
}
|
||||
|
||||
return databaseConnection
|
||||
|
||||
@ -62,14 +62,16 @@ export class BetterSqlite3QueryRunner extends AbstractSqliteQueryRunner {
|
||||
* Called before migrations are run.
|
||||
*/
|
||||
async beforeMigration(): Promise<void> {
|
||||
await this.query(`PRAGMA foreign_keys = OFF`)
|
||||
const databaseConnection = await this.connect()
|
||||
databaseConnection.pragma("foreign_keys = OFF")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after migrations are run.
|
||||
*/
|
||||
async afterMigration(): Promise<void> {
|
||||
await this.query(`PRAGMA foreign_keys = ON`)
|
||||
const databaseConnection = await this.connect()
|
||||
databaseConnection.pragma("foreign_keys = ON")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,10 +174,9 @@ export class BetterSqlite3QueryRunner extends AbstractSqliteQueryRunner {
|
||||
}
|
||||
protected async loadPragmaRecords(tablePath: string, pragma: string) {
|
||||
const [database, tableName] = this.splitTablePath(tablePath)
|
||||
const res = await this.query(
|
||||
`PRAGMA ${
|
||||
database ? `"${database}".` : ""
|
||||
}${pragma}("${tableName}")`,
|
||||
const databaseConnection = await this.connect()
|
||||
const res = databaseConnection.pragma(
|
||||
`${database ? `"${database}".` : ""}${pragma}("${tableName}")`,
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
@ -826,7 +826,11 @@ export class CockroachDriver implements Driver {
|
||||
|
||||
return new Promise((ok, fail) => {
|
||||
this.master.connect((err: any, connection: any, release: any) => {
|
||||
err ? fail(err) : ok([connection, release])
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok([connection, release])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -844,7 +848,11 @@ export class CockroachDriver implements Driver {
|
||||
return new Promise((ok, fail) => {
|
||||
this.slaves[random].connect(
|
||||
(err: any, connection: any, release: any) => {
|
||||
err ? fail(err) : ok([connection, release])
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok([connection, release])
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@ -920,14 +920,20 @@ export class MysqlDriver implements Driver {
|
||||
this.poolCluster.getConnection(
|
||||
"MASTER",
|
||||
(err: any, dbConnection: any) => {
|
||||
err
|
||||
? fail(err)
|
||||
: ok(this.prepareDbConnection(dbConnection))
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok(this.prepareDbConnection(dbConnection))
|
||||
}
|
||||
},
|
||||
)
|
||||
} else if (this.pool) {
|
||||
this.pool.getConnection((err: any, dbConnection: any) => {
|
||||
err ? fail(err) : ok(this.prepareDbConnection(dbConnection))
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok(this.prepareDbConnection(dbConnection))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
fail(
|
||||
@ -951,7 +957,11 @@ export class MysqlDriver implements Driver {
|
||||
this.poolCluster.getConnection(
|
||||
"SLAVE*",
|
||||
(err: any, dbConnection: any) => {
|
||||
err ? fail(err) : ok(this.prepareDbConnection(dbConnection))
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok(this.prepareDbConnection(dbConnection))
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@ -988,14 +988,16 @@ export class OracleDriver implements Driver {
|
||||
try {
|
||||
const oracle = this.options.driver || PlatformTools.load("oracledb")
|
||||
this.oracle = oracle
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new DriverPackageNotInstalledError("Oracle", "oracledb")
|
||||
}
|
||||
const thickMode = this.options.thickMode
|
||||
if (thickMode) {
|
||||
typeof thickMode === "object"
|
||||
? this.oracle.initOracleClient(thickMode)
|
||||
: this.oracle.initOracleClient()
|
||||
if (typeof thickMode === "object") {
|
||||
this.oracle.initOracleClient(thickMode)
|
||||
} else {
|
||||
this.oracle.initOracleClient()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1218,7 +1218,11 @@ export class PostgresDriver implements Driver {
|
||||
|
||||
return new Promise((ok, fail) => {
|
||||
this.master.connect((err: any, connection: any, release: any) => {
|
||||
err ? fail(err) : ok([connection, release])
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok([connection, release])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -1238,7 +1242,11 @@ export class PostgresDriver implements Driver {
|
||||
return new Promise((ok, fail) => {
|
||||
this.slaves[random].connect(
|
||||
(err: any, connection: any, release: any) => {
|
||||
err ? fail(err) : ok([connection, release])
|
||||
if (err) {
|
||||
fail(err)
|
||||
} else {
|
||||
ok([connection, release])
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
@ -1569,14 +1577,17 @@ export class PostgresDriver implements Driver {
|
||||
|
||||
if (options.logNotifications) {
|
||||
connection.on("notice", (msg: any) => {
|
||||
msg && this.connection.logger.log("info", msg.message)
|
||||
if (msg) {
|
||||
this.connection.logger.log("info", msg.message)
|
||||
}
|
||||
})
|
||||
connection.on("notification", (msg: any) => {
|
||||
msg &&
|
||||
if (msg) {
|
||||
this.connection.logger.log(
|
||||
"info",
|
||||
`Received NOTIFY on channel ${msg.channel}: ${msg.payload}.`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
release()
|
||||
|
||||
@ -971,7 +971,7 @@ export class EntityManager {
|
||||
/**
|
||||
* Checks whether any entity exists with the given options.
|
||||
*/
|
||||
exists<Entity extends ObjectLiteral>(
|
||||
async exists<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
options?: FindManyOptions<Entity>,
|
||||
): Promise<boolean> {
|
||||
@ -1002,7 +1002,7 @@ export class EntityManager {
|
||||
* Counts entities that match given options.
|
||||
* Useful for pagination.
|
||||
*/
|
||||
count<Entity extends ObjectLiteral>(
|
||||
async count<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
options?: FindManyOptions<Entity>,
|
||||
): Promise<number> {
|
||||
@ -1020,7 +1020,7 @@ export class EntityManager {
|
||||
* Counts entities that match given conditions.
|
||||
* Useful for pagination.
|
||||
*/
|
||||
countBy<Entity extends ObjectLiteral>(
|
||||
async countBy<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number> {
|
||||
@ -1090,12 +1090,16 @@ export class EntityManager {
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.createQueryBuilder(entityClass, metadata.name)
|
||||
.setFindOptions({ where })
|
||||
const qb = this.createQueryBuilder(entityClass, metadata.name)
|
||||
qb.setFindOptions({ where })
|
||||
|
||||
const alias = qb.alias
|
||||
|
||||
const result = await qb
|
||||
.select(
|
||||
`${fnName}(${this.connection.driver.escape(
|
||||
column.databaseName,
|
||||
)})`,
|
||||
alias,
|
||||
)}.${this.connection.driver.escape(column.databaseName)})`,
|
||||
fnName,
|
||||
)
|
||||
.getRawOne()
|
||||
@ -1140,7 +1144,7 @@ export class EntityManager {
|
||||
* Also counts all entities that match given conditions,
|
||||
* but ignores pagination settings (from and take options).
|
||||
*/
|
||||
findAndCount<Entity extends ObjectLiteral>(
|
||||
async findAndCount<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
options?: FindManyOptions<Entity>,
|
||||
): Promise<[Entity[], number]> {
|
||||
@ -1159,7 +1163,7 @@ export class EntityManager {
|
||||
* Also counts all entities that match given conditions,
|
||||
* but ignores pagination settings (from and take options).
|
||||
*/
|
||||
findAndCountBy<Entity extends ObjectLiteral>(
|
||||
async findAndCountBy<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<[Entity[], number]> {
|
||||
|
||||
@ -15,46 +15,46 @@ import { DeleteResult } from "../query-builder/result/DeleteResult"
|
||||
import { EntityMetadata } from "../metadata/EntityMetadata"
|
||||
|
||||
import {
|
||||
BulkWriteResult,
|
||||
AggregationCursor,
|
||||
Collection,
|
||||
FindCursor,
|
||||
Document,
|
||||
AggregateOptions,
|
||||
AggregationCursor,
|
||||
AnyBulkWriteOperation,
|
||||
BulkWriteOptions,
|
||||
Filter,
|
||||
CountOptions,
|
||||
IndexSpecification,
|
||||
CreateIndexesOptions,
|
||||
IndexDescription,
|
||||
DeleteResult as DeleteResultMongoDb,
|
||||
DeleteOptions,
|
||||
CommandOperationOptions,
|
||||
FindOneAndDeleteOptions,
|
||||
FindOneAndReplaceOptions,
|
||||
UpdateFilter,
|
||||
FindOneAndUpdateOptions,
|
||||
RenameOptions,
|
||||
ReplaceOptions,
|
||||
UpdateResult as UpdateResultMongoDb,
|
||||
BulkWriteResult,
|
||||
ChangeStream,
|
||||
ChangeStreamOptions,
|
||||
Collection,
|
||||
CollStats,
|
||||
CollStatsOptions,
|
||||
ChangeStreamOptions,
|
||||
ChangeStream,
|
||||
UpdateOptions,
|
||||
ListIndexesOptions,
|
||||
ListIndexesCursor,
|
||||
OptionalId,
|
||||
CommandOperationOptions,
|
||||
CountDocumentsOptions,
|
||||
CountOptions,
|
||||
CreateIndexesOptions,
|
||||
DeleteOptions,
|
||||
DeleteResult as DeleteResultMongoDb,
|
||||
Document,
|
||||
Filter,
|
||||
FilterOperators,
|
||||
FindCursor,
|
||||
FindOneAndDeleteOptions,
|
||||
FindOneAndReplaceOptions,
|
||||
FindOneAndUpdateOptions,
|
||||
IndexDescription,
|
||||
IndexInformationOptions,
|
||||
IndexSpecification,
|
||||
InsertManyResult,
|
||||
InsertOneOptions,
|
||||
InsertOneResult,
|
||||
InsertManyResult,
|
||||
UnorderedBulkOperation,
|
||||
OrderedBulkOperation,
|
||||
IndexInformationOptions,
|
||||
ListIndexesCursor,
|
||||
ListIndexesOptions,
|
||||
ObjectId,
|
||||
FilterOperators,
|
||||
CountDocumentsOptions,
|
||||
OptionalId,
|
||||
OrderedBulkOperation,
|
||||
RenameOptions,
|
||||
ReplaceOptions,
|
||||
UnorderedBulkOperation,
|
||||
UpdateFilter,
|
||||
UpdateOptions,
|
||||
UpdateResult as UpdateResultMongoDb,
|
||||
} from "../driver/mongodb/typings"
|
||||
import { DataSource } from "../data-source/DataSource"
|
||||
import { MongoFindManyOptions } from "../find-options/mongodb/MongoFindManyOptions"
|
||||
@ -161,6 +161,16 @@ export class MongoEntityManager extends EntityManager {
|
||||
return this.executeFindAndCount(entityClassOrName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities that match given WHERE conditions.
|
||||
*/
|
||||
async findBy<Entity>(
|
||||
entityClassOrName: EntityTarget<Entity>,
|
||||
where: any,
|
||||
): Promise<Entity[]> {
|
||||
return this.executeFind(entityClassOrName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities by ids.
|
||||
* Optionally find options can be applied.
|
||||
|
||||
@ -12,6 +12,7 @@ import { PropertyTypeFactory } from "./types/PropertyTypeInFunction"
|
||||
import { TypeORMError } from "../error"
|
||||
import { ObjectUtils } from "../util/ObjectUtils"
|
||||
import { InstanceChecker } from "../util/InstanceChecker"
|
||||
import { OrmUtils } from "../util/OrmUtils"
|
||||
|
||||
/**
|
||||
* Contains all information about some entity's relation.
|
||||
@ -520,7 +521,11 @@ export class RelationMetadata {
|
||||
entity,
|
||||
)
|
||||
} else {
|
||||
entity[propertyName] = value
|
||||
if (ObjectUtils.isObject(entity[propertyName])) {
|
||||
OrmUtils.mergeDeep(entity[propertyName], value)
|
||||
} else {
|
||||
entity[propertyName] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -417,6 +417,8 @@ export class MigrationExecutor {
|
||||
this.connection.logger.logSchemaBuild(
|
||||
`No migrations were found in the database. Nothing to revert!`,
|
||||
)
|
||||
// if query runner was created by us then release it
|
||||
if (!this.queryRunner) await queryRunner.release()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,20 @@ export class PlatformTools {
|
||||
return global
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the version string from package.json of the given package.
|
||||
* This operation is only supported in node.
|
||||
*/
|
||||
static readPackageVersion(name: string): string {
|
||||
try {
|
||||
return require(`${name}/package.json`).version
|
||||
} catch (err) {
|
||||
throw new TypeError(
|
||||
`Failed to read package.json for "${name}": ${err.message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads ("require"-s) given file or package.
|
||||
* This operation only supports on node platform
|
||||
|
||||
@ -265,6 +265,39 @@ describe("mongodb > MongoRepository", () => {
|
||||
))
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to use findBy method", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const postRepository = connection.getMongoRepository(Post)
|
||||
|
||||
// save few posts
|
||||
const firstPost = new Post()
|
||||
firstPost.title = "Post #1"
|
||||
firstPost.text = "Everything about post #1"
|
||||
await postRepository.save(firstPost)
|
||||
|
||||
const secondPost = new Post()
|
||||
secondPost.title = "Post #1"
|
||||
secondPost.text = "Everything about post #2"
|
||||
await postRepository.save(secondPost)
|
||||
|
||||
const thirdPost = new Post()
|
||||
thirdPost.title = "Post #2"
|
||||
thirdPost.text = "Everything about post #3"
|
||||
await postRepository.save(thirdPost)
|
||||
|
||||
const loadedPosts = await postRepository.findBy({
|
||||
title: "Post #1",
|
||||
})
|
||||
|
||||
expect(loadedPosts).to.have.length(2)
|
||||
expect(loadedPosts[0]).to.be.instanceOf(Post)
|
||||
expect(loadedPosts[1]).to.be.instanceOf(Post)
|
||||
expect(loadedPosts[0].title).to.eql("Post #1")
|
||||
expect(loadedPosts[1].title).to.eql("Post #1")
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
async function seedPosts(postRepository: MongoRepository<PostWithDeleted>) {
|
||||
|
||||
@ -138,13 +138,14 @@ describe("query builder > distinct on", () => {
|
||||
|
||||
expect(
|
||||
result.map(({ moderator }) => moderator),
|
||||
).to.have.members(["Dion", "Sarah", "Dion", "Dion"]) &&
|
||||
expect(result.map(({ author }) => author)).to.have.members([
|
||||
"Dion",
|
||||
"Pablo",
|
||||
"Sarah",
|
||||
"Zelda",
|
||||
])
|
||||
).to.have.members(["Dion", "Sarah", "Dion", "Dion"])
|
||||
|
||||
expect(result.map(({ author }) => author)).to.have.members([
|
||||
"Dion",
|
||||
"Pablo",
|
||||
"Sarah",
|
||||
"Zelda",
|
||||
])
|
||||
}),
|
||||
))
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { expect } from "chai"
|
||||
import "reflect-metadata"
|
||||
import { DataSource } from "../../../../../src/data-source/DataSource"
|
||||
import {
|
||||
@ -5,11 +6,11 @@ import {
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../../utils/test-utils"
|
||||
import { User } from "./entity/User"
|
||||
import { Profile } from "./entity/Profile"
|
||||
import { Category } from "./entity/Category"
|
||||
import { Editor } from "./entity/Editor"
|
||||
import { Post } from "./entity/Post"
|
||||
import { Category } from "./entity/Category"
|
||||
import { Profile } from "./entity/Profile"
|
||||
import { User } from "./entity/User"
|
||||
|
||||
describe("relations > eager relations > basic", () => {
|
||||
let connections: DataSource[]
|
||||
@ -77,7 +78,7 @@ describe("relations > eager relations > basic", () => {
|
||||
loadedPost!.categories1.sort((a, b) => a.id - b.id)
|
||||
loadedPost!.categories2.sort((a, b) => a.id - b.id)
|
||||
|
||||
loadedPost!.should.be.eql({
|
||||
expect(loadedPost).to.deep.equal({
|
||||
id: 1,
|
||||
title: "about eager relations",
|
||||
categories1: [
|
||||
@ -104,6 +105,7 @@ describe("relations > eager relations > basic", () => {
|
||||
id: 1,
|
||||
firstName: "Timber",
|
||||
lastName: "Saw",
|
||||
deletedAt: null,
|
||||
profile: {
|
||||
id: 1,
|
||||
about: "I cut trees!",
|
||||
@ -117,6 +119,7 @@ describe("relations > eager relations > basic", () => {
|
||||
id: 1,
|
||||
firstName: "Timber",
|
||||
lastName: "Saw",
|
||||
deletedAt: null,
|
||||
profile: {
|
||||
id: 1,
|
||||
about: "I cut trees!",
|
||||
@ -138,10 +141,61 @@ describe("relations > eager relations > basic", () => {
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne()
|
||||
|
||||
loadedPost!.should.be.eql({
|
||||
expect(loadedPost).to.deep.equal({
|
||||
id: 1,
|
||||
title: "about eager relations",
|
||||
})
|
||||
}),
|
||||
))
|
||||
|
||||
it("should preserve manually requested nested relations with DeleteDateColumn", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
await prepareData(connection)
|
||||
|
||||
// Prepare test data - reusing existing entities
|
||||
const nestedProfile = new Profile()
|
||||
nestedProfile.about = "I am nested!"
|
||||
await connection.manager.save(nestedProfile)
|
||||
|
||||
const user = (await connection.manager.findOne(User, {
|
||||
where: { id: 1 },
|
||||
}))!
|
||||
user.nestedProfile = nestedProfile
|
||||
await connection.manager.save(user)
|
||||
|
||||
// Retrieve user with manually specified nested relation
|
||||
const retrievedEditor = await connection.manager.findOne(
|
||||
Editor,
|
||||
{
|
||||
where: { userId: 1 },
|
||||
relations: {
|
||||
user: {
|
||||
nestedProfile: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Assertions
|
||||
expect(retrievedEditor).to.deep.equal({
|
||||
userId: 1,
|
||||
postId: 1,
|
||||
user: {
|
||||
id: 1,
|
||||
firstName: "Timber",
|
||||
lastName: "Saw",
|
||||
deletedAt: null,
|
||||
nestedProfile: {
|
||||
id: 2,
|
||||
about: "I am nested!",
|
||||
},
|
||||
profile: {
|
||||
id: 1,
|
||||
about: "I cut trees!",
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { Entity } from "../../../../../../src/decorator/entity/Entity"
|
||||
import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn"
|
||||
import { Column } from "../../../../../../src/decorator/columns/Column"
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "../../../../../../src"
|
||||
|
||||
@Entity()
|
||||
export class Profile {
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { Entity } from "../../../../../../src/decorator/entity/Entity"
|
||||
import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn"
|
||||
import { Column } from "../../../../../../src/decorator/columns/Column"
|
||||
import { OneToOne } from "../../../../../../src/decorator/relations/OneToOne"
|
||||
import { JoinColumn } from "../../../../../../src/decorator/relations/JoinColumn"
|
||||
import {
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from "../../../../../../src"
|
||||
import { Profile } from "./Profile"
|
||||
|
||||
@Entity()
|
||||
@ -19,4 +23,10 @@ export class User {
|
||||
@OneToOne(() => Profile, { eager: true })
|
||||
@JoinColumn()
|
||||
profile: Profile
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt?: Date
|
||||
|
||||
@ManyToOne(() => Profile)
|
||||
nestedProfile: Profile
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
OneToMany,
|
||||
} from "../../../../../src"
|
||||
import { Post } from "./Post"
|
||||
|
||||
@Entity()
|
||||
export class Author {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@OneToMany(() => Post, (post) => post.author)
|
||||
posts: Post[]
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
} from "../../../../../src"
|
||||
import { Author } from "./Author"
|
||||
|
||||
@Entity()
|
||||
export class Post {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
title: string
|
||||
|
||||
@Column()
|
||||
viewCount: number
|
||||
|
||||
@ManyToOne(() => Author, (author) => author.posts)
|
||||
author: Author
|
||||
}
|
||||
@ -0,0 +1,280 @@
|
||||
import "reflect-metadata"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
import { DataSource } from "../../../../src"
|
||||
import { Post } from "./entity/Post"
|
||||
import { Author } from "./entity/Author"
|
||||
import { expect } from "chai"
|
||||
|
||||
describe("repository > aggregate methods with relations", () => {
|
||||
let connections: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
schemaCreate: true,
|
||||
dropSchema: true,
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
describe("sum with relation filter", () => {
|
||||
it("should return the aggregate sum when filtering by relation", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
const author2 = await authorRepo.save({
|
||||
name: "Author 2",
|
||||
})
|
||||
|
||||
await postRepo.save([
|
||||
{ title: "Post 1", viewCount: 100, author: author1 },
|
||||
{ title: "Post 2", viewCount: 200, author: author1 },
|
||||
{ title: "Post 3", viewCount: 300, author: author2 },
|
||||
])
|
||||
|
||||
const sum = await postRepo.sum("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(sum).to.equal(300)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return null when no records match relation filter", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
|
||||
const sum = await postRepo.sum("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(sum).to.be.equal(null)
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
describe("average with relation filter", () => {
|
||||
it("should return the aggregate average when filtering by relation", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
const author2 = await authorRepo.save({
|
||||
name: "Author 2",
|
||||
})
|
||||
|
||||
await postRepo.save([
|
||||
{ title: "Post 1", viewCount: 100, author: author1 },
|
||||
{ title: "Post 2", viewCount: 200, author: author1 },
|
||||
{ title: "Post 3", viewCount: 300, author: author2 },
|
||||
])
|
||||
|
||||
const average = await postRepo.average("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(average).to.equal(150)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return null when no records match relation filter", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
|
||||
const average = await postRepo.average("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(average).to.be.equal(null)
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
describe("minimum with relation filter", () => {
|
||||
it("should return the aggregate minimum when filtering by relation", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
const author2 = await authorRepo.save({
|
||||
name: "Author 2",
|
||||
})
|
||||
|
||||
await postRepo.save([
|
||||
{ title: "Post 1", viewCount: 100, author: author1 },
|
||||
{ title: "Post 2", viewCount: 200, author: author1 },
|
||||
{ title: "Post 3", viewCount: 50, author: author2 },
|
||||
])
|
||||
|
||||
const minimum = await postRepo.minimum("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(minimum).to.equal(100)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return null when no records match relation filter", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
|
||||
const minimum = await postRepo.minimum("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(minimum).to.be.equal(null)
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
describe("maximum with relation filter", () => {
|
||||
it("should return the aggregate maximum when filtering by relation", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
const author2 = await authorRepo.save({
|
||||
name: "Author 2",
|
||||
})
|
||||
|
||||
await postRepo.save([
|
||||
{ title: "Post 1", viewCount: 100, author: author1 },
|
||||
{ title: "Post 2", viewCount: 200, author: author1 },
|
||||
{ title: "Post 3", viewCount: 500, author: author2 },
|
||||
])
|
||||
|
||||
const maximum = await postRepo.maximum("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(maximum).to.equal(200)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return null when no records match relation filter", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
|
||||
const maximum = await postRepo.maximum("viewCount", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(maximum).to.be.equal(null)
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
describe("aggregate methods with nested relation filters", () => {
|
||||
it("should handle complex relation filters correctly", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "John Doe",
|
||||
})
|
||||
const author2 = await authorRepo.save({
|
||||
name: "Jane Smith",
|
||||
})
|
||||
|
||||
await postRepo.save([
|
||||
{ title: "Post 1", viewCount: 100, author: author1 },
|
||||
{ title: "Post 2", viewCount: 200, author: author1 },
|
||||
{ title: "Post 3", viewCount: 300, author: author2 },
|
||||
])
|
||||
|
||||
// Filter by both relation id and relation property
|
||||
const sum = await postRepo.sum("viewCount", {
|
||||
author: { id: author1.id, name: "John Doe" },
|
||||
})
|
||||
|
||||
expect(sum).to.equal(300)
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
describe("aggregate methods with multiple tables having same column name", () => {
|
||||
it("should correctly qualify column names to avoid ambiguous references", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const authorRepo = connection.getRepository(Author)
|
||||
const postRepo = connection.getRepository(Post)
|
||||
|
||||
const author1 = await authorRepo.save({
|
||||
name: "Author 1",
|
||||
})
|
||||
const author2 = await authorRepo.save({
|
||||
name: "Author 2",
|
||||
})
|
||||
|
||||
await postRepo.save([
|
||||
{ title: "Post 1", viewCount: 100, author: author1 },
|
||||
{ title: "Post 2", viewCount: 200, author: author1 },
|
||||
{ title: "Post 3", viewCount: 300, author: author2 },
|
||||
])
|
||||
|
||||
// Both Post and Author have 'id' column - this should not cause ambiguous column error
|
||||
const maxId = await postRepo.maximum("id", {
|
||||
author: { id: author1.id },
|
||||
})
|
||||
|
||||
expect(maxId).to.be.a("number")
|
||||
expect(maxId).to.be.greaterThan(0)
|
||||
|
||||
// Verify we got the correct max ID from author1's posts, not from author2 or any other table
|
||||
const author1Posts = await postRepo.find({
|
||||
where: { author: { id: author1.id } },
|
||||
order: { id: "DESC" },
|
||||
})
|
||||
expect(maxId).to.equal(author1Posts[0].id)
|
||||
}),
|
||||
))
|
||||
})
|
||||
})
|
||||
30
test/github-issues/11231/issue-11231.test.ts
Normal file
30
test/github-issues/11231/issue-11231.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { expect } from "chai"
|
||||
import "reflect-metadata"
|
||||
import { DataSource } from "../../../src/data-source/DataSource"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
} from "../../utils/test-utils"
|
||||
|
||||
describe("github issues > #11231 Error trying to revert last migration when there is none on Oracle", () => {
|
||||
let dataSources: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(dataSources = await createTestingConnections({
|
||||
entities: [],
|
||||
enabledDrivers: ["oracle"],
|
||||
migrations: [],
|
||||
schemaCreate: false,
|
||||
dropSchema: true,
|
||||
})),
|
||||
)
|
||||
after(() => closeTestingConnections(dataSources))
|
||||
|
||||
it("should not throw when migrations list is empty", () =>
|
||||
Promise.all(
|
||||
dataSources.map(async (dataSource) => {
|
||||
await dataSource.undoLastMigration()
|
||||
expect(await dataSource.destroy()).to.not.throw
|
||||
}),
|
||||
))
|
||||
})
|
||||
108
test/unit/cache/redis-query-result-cache.ts
vendored
Normal file
108
test/unit/cache/redis-query-result-cache.ts
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import { RedisQueryResultCache } from "../../../src/cache/RedisQueryResultCache"
|
||||
import { PlatformTools } from "../../../src/platform/PlatformTools"
|
||||
import { DataSource } from "../../../src/data-source/DataSource"
|
||||
|
||||
describe("RedisQueryResultCache", () => {
|
||||
describe("detectRedisVersion", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockDataSource: sinon.SinonStubbedInstance<DataSource>
|
||||
let readPackageVersionStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create a mock DataSource
|
||||
mockDataSource = {
|
||||
options: {},
|
||||
logger: {
|
||||
log: sandbox.stub(),
|
||||
},
|
||||
} as any
|
||||
|
||||
// Stub PlatformTools.readPackageVersion
|
||||
readPackageVersionStub = sandbox.stub(
|
||||
PlatformTools,
|
||||
"readPackageVersion",
|
||||
)
|
||||
|
||||
// Stub PlatformTools.load to prevent actual redis loading
|
||||
sandbox.stub(PlatformTools, "load").returns({})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("should detect Redis v3.x and set redisMajorVersion to 3", () => {
|
||||
readPackageVersionStub.returns("3.1.2")
|
||||
|
||||
const cache = new RedisQueryResultCache(
|
||||
mockDataSource as any,
|
||||
"redis",
|
||||
)
|
||||
|
||||
// Access the private method via any cast for testing
|
||||
;(cache as any).detectRedisVersion()
|
||||
|
||||
expect((cache as any).redisMajorVersion).to.equal(3)
|
||||
expect(readPackageVersionStub.calledOnceWith("redis")).to.be.true
|
||||
})
|
||||
|
||||
it("should detect Redis v4.x and set redisMajorVersion to 3 (callback-based)", () => {
|
||||
readPackageVersionStub.returns("4.6.13")
|
||||
|
||||
const cache = new RedisQueryResultCache(
|
||||
mockDataSource as any,
|
||||
"redis",
|
||||
)
|
||||
|
||||
;(cache as any).detectRedisVersion()
|
||||
|
||||
expect((cache as any).redisMajorVersion).to.equal(3)
|
||||
})
|
||||
|
||||
it("should detect Redis v5.x and set redisMajorVersion to 5 (Promise-based)", () => {
|
||||
readPackageVersionStub.returns("5.0.0")
|
||||
|
||||
const cache = new RedisQueryResultCache(
|
||||
mockDataSource as any,
|
||||
"redis",
|
||||
)
|
||||
|
||||
;(cache as any).detectRedisVersion()
|
||||
|
||||
expect((cache as any).redisMajorVersion).to.equal(5)
|
||||
expect(readPackageVersionStub.calledOnceWith("redis")).to.be.true
|
||||
})
|
||||
|
||||
it("should detect Redis v6.x and set redisMajorVersion to 5 (Promise-based)", () => {
|
||||
readPackageVersionStub.returns("6.2.3")
|
||||
|
||||
const cache = new RedisQueryResultCache(
|
||||
mockDataSource as any,
|
||||
"redis",
|
||||
)
|
||||
|
||||
;(cache as any).detectRedisVersion()
|
||||
|
||||
expect((cache as any).redisMajorVersion).to.equal(5)
|
||||
})
|
||||
|
||||
it("should detect Redis v7.x and set redisMajorVersion to 5 (Promise-based)", () => {
|
||||
readPackageVersionStub.returns("7.0.0")
|
||||
|
||||
const cache = new RedisQueryResultCache(
|
||||
mockDataSource as any,
|
||||
"redis",
|
||||
)
|
||||
|
||||
;(cache as any).detectRedisVersion()
|
||||
|
||||
expect((cache as any).redisMajorVersion).to.equal(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
62
test/unit/platform/platform-tools.ts
Normal file
62
test/unit/platform/platform-tools.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import { PlatformTools } from "../../../src/platform/PlatformTools"
|
||||
|
||||
describe("PlatformTools", () => {
|
||||
describe("readPackageVersion", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("should successfully read version from an installed package", () => {
|
||||
// Test with a package we know exists in node_modules (chai is in devDependencies)
|
||||
const version = PlatformTools.readPackageVersion("chai")
|
||||
|
||||
expect(version).to.be.a("string")
|
||||
expect(version).to.match(/^\d+\.\d+\.\d+/)
|
||||
})
|
||||
|
||||
it("should return correct version format with major.minor.patch", () => {
|
||||
const version = PlatformTools.readPackageVersion("chai")
|
||||
|
||||
expect(version).to.be.a("string")
|
||||
expect(version.split(".").length).to.be.at.least(3)
|
||||
})
|
||||
|
||||
it("should throw TypeError when package does not exist", () => {
|
||||
expect(() => {
|
||||
PlatformTools.readPackageVersion(
|
||||
"this-package-definitely-does-not-exist-12345",
|
||||
)
|
||||
}).to.throw(
|
||||
TypeError,
|
||||
/Failed to read package\.json for "this-package-definitely-does-not-exist-12345"/,
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle scoped package names", () => {
|
||||
const version = PlatformTools.readPackageVersion("@types/node")
|
||||
|
||||
expect(version).to.be.a("string")
|
||||
expect(version).to.match(/^\d+/)
|
||||
})
|
||||
|
||||
it("should throw error for empty package name", () => {
|
||||
expect(() => {
|
||||
PlatformTools.readPackageVersion("")
|
||||
}).to.throw(TypeError, /Failed to read package\.json/)
|
||||
})
|
||||
|
||||
it("should throw error for whitespace-only package name", () => {
|
||||
expect(() => {
|
||||
PlatformTools.readPackageVersion(" ")
|
||||
}).to.throw(TypeError, /Failed to read package\.json/)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user