Query runner CREATE FUNCTION does not work in TypeORM migrations - typeorm

I have written a migration to create a function in my database
import { MigrationInterface, QueryRunner } from 'typeorm';
export class ResetStaging1660754690269 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
/**
* Custom postgresql function to generate date dimension id
*/
await queryRunner.query(`DROP FUNCTION IF EXISTS date_dim_id()`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION date_dim_id() RETURNS int AS
$$
SELECT to_char(now(),'YYYYMMDD')::INT;
$$
LANGUAGE SQL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP FUNCTION IF EXISTS date_dim_id()`);
}
}
However, it seems that it is not being created and I get this error:
(node:29357) UnhandledPromiseRejectionWarning: QueryFailedError: function date_dim_id() does not exist
For now, I am creating it manually by hand but this defeats the purpose of having the migration.

Related

type 'Null' is not a subtype of type 'Future<DataState<List<CustomerEntity>>>

I'm working on a test CRUD application using Bloc for state management and sqflite database. I was asked to implement BDD testing for the app but I have no idea about BDD testing. From what I've learned so far I tryed to implement a simple scenario for the start which is landing on the home screen, but I'm getting this error when I run the test.
Also I don't know exactly how to mock my database to test the four main Create, Read, Update, and Delete functionalities.
I'm using getIt for dependency injection, Mocktail, and bdd_widget_test.
It is the scenario that I wrote in the .feature file:
Feature: Add Feature
Scenario: Landing on the home screen
Given the app is running
Then I see enabled elevated button
And it's the logic for the test.dart file where I get the mentioned exception:
class MockGetAllCustomers extends Mock implements GetAllCustomersUsecase {}
class MockAddCustomer extends Mock implements AddCustomerUsecase {}
class MockUpdateCustomer extends Mock implements UpdateCustomerUsecase {}
class MockDeleteCustomer extends Mock implements DeleteCustomerUsecase {}
class MockDbHelper extends Mock implements DBHelper {}
void main() {
final GetIt getIt = GetIt.instance;
late CustomersBloc bloc;
late MockGetAllCustomers mockGetAllCustomers;
late MockAddCustomer mockAddCustomer;
late MockUpdateCustomer mockUpdateCustomer;
late MockDeleteCustomer mockDeleteCustomer;
late MockDbHelper dbHelper;
late Database database;
CustomerEntity customer = CustomerEntity(
firstName: 'firstName',
lastName: 'lastName',
dateOfBirth: 'dateOfBirth',
phoneNumber: 'phoneNumber',
email: 'email',
bankAccountNumber: 'bankAccountNumber');
setUpAll(() async {
// Initialize FFI
sqfliteFfiInit();
database = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
await database.execute(
'CREATE TABLE $customersTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colFirstName TEXT, $colLastName TEXT, $colDateOfBirth TEXT, $colPhoneNumber TEXT, $colEmail TEXT, $colAccountNum TEXT)');
dbHelper = MockDbHelper();
dbHelper.database = database;
databaseFactory = databaseFactoryFfi;
});
setUp(() {
mockGetAllCustomers = MockGetAllCustomers();
mockAddCustomer = MockAddCustomer();
mockUpdateCustomer = MockUpdateCustomer();
mockDeleteCustomer = MockDeleteCustomer();
bloc = CustomersBloc(mockGetAllCustomers, mockAddCustomer,
mockUpdateCustomer, mockDeleteCustomer);
getIt.registerFactory(() => bloc);
});
group('''Add Feature''', () {
testWidgets('''Landing on the home screen''', (tester) async {
await theAppIsRunning(tester);
await iSeeEnabledElevatedButton(tester);
});
});
}
It's a part of the exception message I'm receiving:
type 'Null' is not a subtype of type 'Future<DataState<List<CustomerEntity>>>'
package:mc_crud_test/features/customer_feature/domain/usecases/get_all_customers_usecase.dart 11:43 MockGetAllCustomers.execute
package:mc_crud_test/features/customer_feature/presentation/bloc/bloc/customers_bloc.dart 43:60 new CustomersBloc.<fn>
package:bloc/src/bloc.dart 226:26 Bloc.on.<fn>.handleEvent
package:bloc/src/bloc.dart 235:9 Bloc.on.<fn>
dart:async
And it's my database class:
String customersTable = 'customers_table';
String colId = 'id';
String colFirstName = 'firstName';
String colLastName = 'lastName';
String colDateOfBirth = 'dateOfBirth';
String colPhoneNumber = 'phoneNumber';
String colEmail = 'email';
String colAccountNum = 'bankAccountNumber';
//
class DBHelper {
//
Database database;
DBHelper({required this.database});
//Database initialization and creation
static Future<Database> initDatabase() async {
//
final dbPath = await sql.getDatabasesPath();
return await sql.openDatabase(
path.join(dbPath, 'customers.db'),
onCreate: (db, version) {
return db.execute(
'CREATE TABLE $customersTable($colId INTEGER PRIMARY KEY AUTOINCREMENT, $colFirstName TEXT, $colLastName TEXT, $colDateOfBirth TEXT, $colPhoneNumber TEXT, $colEmail TEXT, $colAccountNum TEXT)');
},
version: 1,
);
}
//
//find a customer by firstName, lastName, dateOfBirth, and email
Future<bool> findCustomer(CustomerEntity customer) async {
//
List<Map<String, dynamic>> map = await database.query(customersTable,
columns: [colFirstName, colLastName, colDateOfBirth, colEmail],
where:
'$colFirstName = ? OR $colLastName = ? OR $colDateOfBirth = ? OR $colEmail = ?',
whereArgs: [
customer.firstName,
customer.lastName,
customer.dateOfBirth,
customer.email
]);
if (map.isEmpty) {
return false;
} else {
return true;
}
}
// Add a customer to the Database
Future<int> insertCustomer(CustomerEntity customer) async {
//
final id = await database.insert(
customersTable,
customer.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
return id;
}
// Get the list of customers
Future<List<CustomerEntity>> getAllCustomers() async {
//
try {
final List<Map<String, Object?>> queryResult = await database.query(
customersTable,
orderBy: colId,
);
return queryResult.isEmpty
? []
: queryResult.map((e) => CustomerEntity.fromMapObject(e)).toList();
} catch (e) {
print('Error reading database : $e');
return [];
}
}
//
// Delete a Customer
Future<int> deleteCustomer(int id) async {
return await database
.delete(customersTable, where: '$colId = ?', whereArgs: [id]);
}
//
//Update a Customer
Future<bool> updateCustomer(CustomerEntity customer) async {
try {
final count = await database.update(customersTable, customer.toMap(),
where: '$colId = ?', whereArgs: [customer.id]);
if (count == 1) {
return true;
} else {
return false;
}
} catch (e) {
print('Failed to update the customer: $e');
return false;
}
}
}

NestJS - inject service into typeorm migration

I would like to inject a service into a typeorm migration, so that I can perform data migration based on some logic within a service:
import { MigrationInterface, QueryRunner, Repository } from 'typeorm';
import { MyService } from '../../services/MyService.service';
import { MyEntity } from '../../entities/MyEntity.entity';
export class MyEntityMigration12345678
implements MigrationInterface
{
name = 'MyEntityMigration12345678';
constructor(
private readonly myService: MyService,
) {}
public async up(queryRunner: QueryRunner): Promise<void> {
const myEntityRepository: Repository<MyEntity> =
queryRunner.connection.getRepository<MyEntity>(MyEntity);
const entities = await myEntityRepository.findBy({
myColumn: '',
});
for (const entity of entities) {
const columnValue = this.myService.getColumnValue(myEntity.id);
await myEntityRepository.save({
...entity,
myColumn: columnValue,
});
}
}
// ...
}
Nevertheless
myService is undefined, and
myEntityRepository.findBy(.) gets stuck.
How can I do a migration based on business logic?
Thanks!
One option would be to write whatever query myService.getColumn value does inside your migration. If you're hell bent on using Nest's DI inside your migration then you could do something like this:
import { NestFactory } from '#nestjs/core';
import { MigrationInterface, QueryRunner, Repository } from 'typeorm';
import { AppModule } from '../../app.module'; // assumed path
import { MyService } from '../../services/MyService.service';
import { MyEntity } from '../../entities/MyEntity.entity';
export class MyEntityMigration12345678
implements MigrationInterface
{
name = 'MyEntityMigration12345678';
public async up(queryRunner: QueryRunner): Promise<void> {
const myEntityRepository: Repository<MyEntity> =
queryRunner.connection.getRepository<MyEntity>(MyEntity);
const entities = await myEntityRepository.findBy({
myColumn: '',
});
const appCtx = await NestFactory.createApplicationContext(AppModule);
const myService = app.get(MyService, { strict: false });
for (const entity of entities) {
const columnValue = myService.getColumnValue(myEntity.id);
await myEntityRepository.save({
...entity,
myColumn: columnValue,
});
}
await app.close();
}
// ...
}
You can't use injection inside the migration because the class itself is managed by TypeORM. You can, as shown above, create a NestApplicationContext instance and get the MyService instance from that. This only works, by the way, if MyService is REQUEST scoped

Delete with query builder?

I have some services I want to remove from my database by using query builder, but the terminal shows me this error:
Error during migration run:
TypeError: this.subQuery is not a function
Any idea of what might be? This is my query I'm trying to use to delete some services:
public async down(queryRunner: QueryRunner): Promise<void> {
// Do nothing
}
private async loadCSV(entity: string, queryRunner: QueryRunner) {
const rowsToInsert = []
const readStream = fs.createReadStream(path.join(__dirname, '/1664316786331/', `${entity}.csv`)).pipe(csv())
for await (const row of readStream) {
Object.keys(row).forEach((key) => {
if ((row as any)[key] === '') (row as any)[key] = null
})
// rowsToDelete
await getConnection()
.createQueryBuilder()
.delete()
.from(row)
.where('idServiceSchedule=:idServiceSchedule', {
idServiceSchedule: row.idServiceSchedule
})
.printSql()
.execute()
}
// await queryRunner.connection.getRepository(entity).save(rowsToInsert)
console.log(readStream)
}
}

The return type 'Database' isn't a 'Future<void>',

Hello I am getting this error
I want to return the database object for the function database
I am getting this error The return type 'Database' isn't a 'Future < void >'
dart
Future < Database > database() async {
return openDatabase(
join( await getDatabasesPath(), 'todo.db'),
onCreate: (db, version) async {
await db.execute("CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT, description TEXT)");
await db.execute("CREATE TABLE todo(id INTEGER PRIMARY KEY, taskId INTEGER, title TEXT, status INTEGER)");
return db ;
},
version: 1,
);
}
Here is my code
I want to access that database function for my future function
Here is my Sample Next function
Future<int> insertTask(Task task) async {
int taskId = 0;
Database _db = await database();
await _db.insert('tasks', task.toMap(), conflictAlgorithm: ConflictAlgorithm.replace).then((value) {
taskId = value;
});
return taskId;
}
Another Function
Future<void> insertTodo(Todo todo) async {
Database _db = await database();
await _db.insert('todo', todo.toMap(), conflictAlgorithm: ConflictAlgorithm.replace);
}
Print("Hi")
Try this:
Future <Database> database() async {
return await openDatabase(
join( await getDatabasesPath(), 'todo.db'),
onCreate: (db, version) async {
await db.execute("CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT, description TEXT)");
await db.execute("CREATE TABLE todo(id INTEGER PRIMARY KEY, taskId INTEGER, title TEXT, status INTEGER)");
},
version: 1,
);
}

How to handle TypeORM entity field unique validation error in NestJS?

I've set a custom unique validator decorator on my TypeORM entity field email. NestJS has dependency injection, but the service is not injected.
The error is:
TypeError: Cannot read property 'findByEmail' of undefined
Any help on implementing a custom email validator?
user.entity.ts:
#Column()
#Validate(CustomEmail, {
message: "Title is too short or long!"
})
#IsEmail()
email: string;
My CustomEmail validator is
import {ValidatorConstraint, ValidatorConstraintInterface,
ValidationArguments} from "class-validator";
import {UserService} from "./user.service";
#ValidatorConstraint({ name: "customText", async: true })
export class CustomEmail implements ValidatorConstraintInterface {
constructor(private userService: UserService) {}
async validate(text: string, args: ValidationArguments) {
const user = await this.userService.findByEmail(text);
return !user;
}
defaultMessage(args: ValidationArguments) {
return "Text ($value) is too short or too long!";
}
}
I know I could set unique in the Column options
#Column({
unique: true
})
but this throws a mysql error and the ExceptionsHandler that crashes my app, so I can't handle it myself...
Thankx!
I can propose 2 different approaches here, the first one catches the constraint violation error locally without additional request, and the second one uses a global error filter, catching such errors in the entire application. I personally use the latter.
Local no-db request solution
No need to make additional database request. You can catch the error violating the unique constraint and throw any HttpException you want to the client. In users.service.ts:
public create(newUser: Partial<UserEntity>): Promise<UserEntity> {
return this.usersRepository.save(newUser).catch((e) => {
if (/(email)[\s\S]+(already exists)/.test(e.detail)) {
throw new BadRequestException(
'Account with this email already exists.',
);
}
return e;
});
}
Which will return:
Global error filter solution
Or even create a global QueryErrorFilter:
#Catch(QueryFailedError)
export class QueryErrorFilter extends BaseExceptionFilter {
public catch(exception: any, host: ArgumentsHost): any {
const detail = exception.detail;
if (typeof detail === 'string' && detail.includes('already exists')) {
const messageStart = exception.table.split('_').join(' ') + ' with';
throw new BadRequestException(
exception.detail.replace('Key', messageStart),
);
}
return super.catch(exception, host);
}
}
Then in main.ts:
async function bootstrap() {
const app = await NestFactory.create(/**/);
/* ... */
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new QueryErrorFilter(httpAdapter));
/* ... */
await app.listen(3000);
}
bootstrap();
This will give generic $table entity with ($field)=($value) already exists. error message. Example:
I have modified my code. I am checking the uniqueness of username/email in the user service (instead of a custom validator) and return an HttpExcetion in case the user is already inserted in the DB.
The easiest solution!
#Entity()
export class MyEntity extends BaseEntity{
#Column({unique:true}) name:string;
}
export abstract class BaseDataService<T> {
constructor(protected readonly repo: Repository<T>) {}
private async isUnique(t: any) {
const uniqueColumns = this.repo.metadata.uniques.map(
(e) => e.givenColumnNames[0]
);
for (const u of uniqueColumns) {
const count = await this.repo.count({ where: { [u]: ILike(t[u]) } });
if (count > 0) {
throw new UnprocessableEntityException(`${u} must be unique!`);
}
}
}
async save(body: DeepPartial<T>) {
await this.isUnique(body);
try {
return await this.repo.save(body);
} catch (err) {
throw new UnprocessableEntityException(err.message);
}
}
async update(id: number, updated: QueryDeepPartialEntity<T>) {
await this.isUnique(updated)
try {
return await this.repo.update(id, updated);
} catch (err) {
throw new UnprocessableEntityException(err.message);
}
}
}
An approach that works for modern version of NestJS which is based in Daniel Kucal's answer and actually returns the error to the frontend when calling the JSON API is the following:
import {
Catch,
ArgumentsHost,
BadRequestException,
HttpException,
} from '#nestjs/common';
import { BaseExceptionFilter } from '#nestjs/core';
import { QueryFailedError } from 'typeorm';
type ExceptionType = { detail: string; table: string };
#Catch(QueryFailedError)
export class QueryErrorFilter extends BaseExceptionFilter<
HttpException | ExceptionType
> {
public catch(exception: ExceptionType, host: ArgumentsHost): void {
const { detail = null } = exception || {};
if (
!detail ||
typeof detail !== 'string' ||
// deepcode ignore AttrAccessOnNull: <False positive>
!detail.includes('already exists')
) {
return super.catch(exception, host);
} // else
/**
* this regex transform the message `(phone)=(123)` to a more intuitive `with phone: "123"` one,
* the regex is long to prevent mistakes if the value itself is ()=(), for example, (phone)=(()=())
*/
const extractMessageRegex =
/\((.*?)(?:(?:\)=\()(?!.*(\))(?!.*\))=\()(.*?)\)(?!.*\)))(?!.*(?:\)=\()(?!.*\)=\()((.*?)\))(?!.*\)))/;
const messageStart = `${exception.table.split('_').join(' ')} with`;
/** prevent Regex DoS, doesn't treat messages longer than 200 characters */
const exceptionDetail =
exception.detail.length <= 200
? exception.detail.replace(extractMessageRegex, 'with $1: "$3"')
: exception.detail;
super.catch(
new BadRequestException(exceptionDetail.replace('Key', messageStart)),
host,
);
}
}
Also, not forgetting main.ts:
async function bootstrap() {
const app = await NestFactory.create(/**/);
/* ... */
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new QueryErrorFilter(httpAdapter));
/* ... */
await app.listen(3000);
}
bootstrap();

Resources