Compare commits

...

9 Commits

Author SHA1 Message Date
Lucian Mocanu
2133c97437
chore: disable eslint errors for chai assertions (#11833) 2025-12-08 15:10:10 +01:00
Wonbin Choi
cc07c90f1d
fix: add async to the method using setFindOptions() (#10787)
Co-authored-by: Giorgio Boa <35845425+gioboa@users.noreply.github.com>
Co-authored-by: Naor Peled <me@naor.dev>
2025-12-06 21:50:43 +02:00
mjr128
a46eb0a7e1
fix: release query runner when there is no migration to revert (#11232)
Co-authored-by: Adrien PEREZ <adrien-perez@samse.fr>
2025-12-06 15:36:34 +01:00
LeviHeber
2d8c5158db
fix: prevent eager-loaded entities from overwriting manual relations (#11267)
Co-authored-by: Lucian Mocanu <alumni@users.noreply.github.com>
Co-authored-by: Giorgio Boa <35845425+gioboa@users.noreply.github.com>
2025-12-05 14:19:02 +01:00
Prakhar Chhalotre
6e34756b9d
fix: fix up aggregate methods ambiguous column (#11822) 2025-12-04 22:49:31 +01:00
Mike Guida
73fda419e4
chore: release v0.3.28 (#11816) 2025-12-03 09:29:32 +01:00
ibrahim menem
6f486e5a67
fix(redis): version detection logic (#11815)
Co-authored-by: AdolfodelSel <adolfo.selllano@gmail.com>
Co-authored-by: Giorgio Boa <35845425+gioboa@users.noreply.github.com>
Co-authored-by: Mike Guida <mike@mguida.com>
2025-12-02 11:23:26 -07:00
CHOIJEWON
38715bbd41
fix(mongodb): add missing findBy method to MongoEntityManager (#11814) 2025-12-02 16:35:42 +01:00
Mohamed Akram
ec3ea10b44
refactor: use pragma method in better-sqlite3 (#10684) 2025-12-01 22:43:06 +01:00
28 changed files with 863 additions and 133 deletions

View File

@ -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)

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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
}
}

View File

@ -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))
}
},
)
})

View File

@ -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

View File

@ -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
}

View File

@ -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])
}
},
)
})

View File

@ -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))
}
},
)
})

View File

@ -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()
}
}
}

View File

@ -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()

View File

@ -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]> {

View File

@ -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.

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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>) {

View File

@ -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",
])
}),
))

View File

@ -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!",
},
},
})
}),
))
})

View File

@ -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 {

View File

@ -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
}

View File

@ -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[]
}

View File

@ -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
}

View File

@ -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)
}),
))
})
})

View 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
}),
))
})

View 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)
})
})
})

View 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/)
})
})
})