Неожиданные открытия в TypeORM 0.3.11
25 января 2023 г.TypeORM — это инструмент объектно-реляционного сопоставления (ORM), предназначенный для работы с TypeScript. Он позволяет разработчикам взаимодействовать с базами данных с использованием объектно-ориентированного синтаксиса и поддерживает широкий спектр популярных баз данных, включая MySQL, PostgreSQL и SQLite. Но, скорее всего, вы это уже знаете. Так что смысл этой статьи не в том, чтобы объяснить, что такое TypeORM, а в том, чтобы показать некоторые неожиданные открытия, которые я сделал, работая с ним.
Обновление TypeORM до версии 0.3.11
Недавно я работал над проектом, использующим TypeORM. Проект сильно устарел, а версия TypeORM была 0.2.*. Решил обновить до последней версии 0.3.11. Я ожидал найти некоторые критические изменения, и они действительно их внесли.
Что мне понравилось в новой версии
Скажем несколько слов о несовместимых изменениях, которые мне понравились.
Прежде всего, .findOne()
из findOneBy()
больше не будет возвращать undefined
. Вместо этого эти методы будут возвращать null
. Это хорошее изменение, null
— это значение, а результат null
означает, что мы что-то сделали, но получили пустой результат. undefined
, с другой стороны, является типом, и это означает, что мы ничего не делали. Так что это хорошее изменение.
Давайте посмотрим на пример:
import {Entity, Column, PrimaryGeneratedColumn} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
}
Старое поведение:
const user = await User.findOne({name: "the name we don't have in the database"});
console.log(user); // undefined
Текущее поведение:
const user = await User.findOne({name: "the name we don't have in the database"});
console.log(user); // null
Что меня устраивает в новой версии
Далее изменения в API связаны с методом .find()
. Старая версия была более гибкой и позволяла передавать только параметры where
или весь объект FindOptions
. Новая версия требует передачи всего объекта FindOptions
. Новая версия является строгой, и вы всегда должны передавать объект FindOptions
целиком.
Старое поведение:
const users = await User.find({
name: "Alice"
})
Новый:
const users = await User.find({
where: {name: "Alice"}
})
К счастью для нас, в новой версии есть новый метод с именем .findMy()
, который принимает только параметры where
.
const users = await User.findBy({
name: "Alice"
})
Что мне не нравится в новой версии
И последнее, но не менее важное: изменения в обработке значений null
и undefined
параметров where. Это стало для меня неожиданностью, так как я не смог найти никакой информации об этом изменении в документации и журнале изменений. И на самом деле я наткнулся на это изменение только на стадии тестирования. В целом, приложение, над которым я работал, имеет впечатляющее тестовое покрытие (более 90%), а REST API имеет довольно хороший уровень проверки. Поэтому отправить параметр null
или undefined
в API непросто. Итак, позвольте мне перейти к самой проблеме.
Давайте представим, что в некоторых местах у нас нет надлежащей проверки параметров запроса, и мы можем отправить в API null
или undefined
.
Давайте посмотрим на пример:
function findUserByEmail(email: string): Promise<User | null> {
return User.findOneBy({
email: email
});
}
В идеальном мире Typescript эта функция выглядит просто отлично, и невозможно напрямую передать null
или undefined
в коде — это нарушит компиляцию. Но в конце концов он будет скомпилирован в JavaScript, и итоговый код будет выглядеть примерно так:
function findUserByEmail(email) {
return User.findOneBy({
email: email
});
}
Таким образом, можно передать null
или undefined
в функцию findUserByEmail
.
А теперь давайте посмотрим на сгенерированные SQL-запросы для следующих вызовов функций.
Для действительного параметра электронной почты обе версии TypeORM будут генерировать один и тот же запрос:
select *
from
user
where
email = :email limit 1
Я упрощаю запрос, но суть в том, что запрос тот же.
А вот для параметров null
и undefined
запрос в новой версии будет другим:
select *
from
user limit 1
Как видите, этот запрос вернет первого пользователя из базы данных. А это не то, чего мы хотим.
Причина этого в том, что разработчики TypeORM решили пропустить предложение where
, если параметр where
имеет значение null или не определено. В предыдущей версии такие параметры вызывали ошибку, и запрос не выполнялся. Но в новой версии все будет пропущено, и запрос будет выполняться без предложения where
. С этим связана проблема на Github, но, похоже, она не будет исправлена в ближайшее время. https://github.com/typeorm/typeorm/issues/9316
Поэтому вместо того, чтобы ждать исправления, я решил написать небольшой класс-оболочку, чтобы обеспечить дополнительный уровень проверки параметров where
. Класс-оболочка проверит, является ли параметр where
null
или undefined
, и выдаст ошибку.
import {BaseEntity, FindOneOptions} from 'typeorm'
import {FindManyOptions} from 'typeorm/find-options/FindManyOptions'
import {FindOptionsWhere} from 'typeorm/find-options/FindOptionsWhere'
interface Constructable {
new(...args: any[]): any
}
function paramsCheckingMixin<T extends Constructable>(base: T): T {
return class extends base {
/**
* Counts entities that match given options.
*/
static async count<T extends BaseEntity>(
this: { new(): T } & typeof BaseEntity,
options?: FindManyOptions<T>
): Promise<number> {
await validateWhereOptions(options?.where)
return this.getRepository<T>().count(options as FindManyOptions<BaseEntity>)
}
/**
* Counts entities that match given WHERE conditions.
*/
static async countBy<T extends BaseEntity>(
this: { new(): T } & typeof BaseEntity,
where: FindOptionsWhere<T>
): Promise<number> {
await validateWhereOptions(where)
return this.getRepository<T>().countBy(where as FindOptionsWhere<BaseEntity>)
}
/**
* Finds entities that match given options.
*/
static async find<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
options?: FindManyOptions<T>
): Promise<T[]> {
await validateWhereOptions(options?.where)
return this.getRepository<T>().find(options)
}
/**
* Finds entities that match given WHERE conditions.
*/
static async findBy<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
where: FindOptionsWhere<T>
): Promise<T[]> {
await validateWhereOptions(where)
return this.getRepository<T>().findBy(where)
}
/**
* Finds entities that match given find options.
* Also counts all entities that match given conditions,
* but ignores pagination settings (from and take options).
*/
static async findAndCount<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
options?: FindManyOptions<T>
): Promise<[T[], number]> {
await validateWhereOptions(options?.where)
return this.getRepository<T>().findAndCount(options)
}
/**
* Finds entities that match given WHERE conditions.
* Also counts all entities that match given conditions,
* but ignores pagination settings (from and take options).
*/
static async findAndCountBy<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
where: FindOptionsWhere<T>
): Promise<[T[], number]> {
await validateWhereOptions(where)
return this.getRepository<T>().findAndCountBy(where)
}
/**
* Finds first entity that matches given conditions.
*/
static async findOne<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
options: FindOneOptions<T>
): Promise<T | null> {
await validateWhereOptions(options?.where)
return this.getRepository<T>().findOne(options)
}
/**
* Finds first entity that matches given conditions.
*/
static async findOneBy<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
where: FindOptionsWhere<T>
): Promise<T | null> {
await validateWhereOptions(where)
return this.getRepository<T>().findOneBy(where)
}
/**
* Finds first entity that matches given conditions.
*/
static async findOneOrFail<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
options: FindOneOptions<T>
): Promise<T> {
await validateWhereOptions(options?.where)
return this.getRepository<T>().findOneOrFail(options)
}
/**
* Finds first entity that matches given conditions.
*/
static async findOneByOrFail<T extends AppBaseEntity>(
this: { new(): T } & typeof AppBaseEntity,
where: FindOptionsWhere<T>
): Promise<T> {
await validateWhereOptions(where)
return this.getRepository<T>().findOneByOrFail(where)
}
}
}
export abstract class AppBaseEntity extends paramsCheckingMixin(BaseEntity) {
}
function validateWhereOptions(where?: FindOptionsWhere<BaseEntity>[] | FindOptionsWhere<BaseEntity>): Promise<void> {
if (!where) {
return Promise.resolve()
}
if (!Array.isArray(where)) {
where = [where]
}
const errors: string[] = []
where.forEach((findOptionsWhere) => {
for (const key in findOptionsWhere) {
if (findOptionsWhere[key] === null || findOptionsWhere[key] === undefined) {
errors.push(`Invalid value of where parameter ${key}`)
}
}
})
if (errors.length) {
return Promise.reject(errors.join('. '))
}
return Promise.resolve()
}
Класс-оболочка используется в качестве базового класса для всех сущностей.
Заключение
Я считаю, что безопасность — очень важная часть любого приложения, поскольку мы все заботимся о наших пользователях и их данных. К сожалению, его часто упускают из виду, и разработчики не уделяют ему должного внимания. Я надеюсь, что эта статья поможет вам улучшить безопасность вашего приложения на уровне базы данных и поможет избежать некоторых распространенных ошибок.
Оригинал