Reputation: 941
I have a project that's using nestjs
with typeorm
.
There's a database.config.ts
:
const entitiesPath = join(__dirname, '..', 'entities', '*.entity.{ts,js}');
const migrationsPath = join(__dirname, '..', 'migrations', '*.*');
const subscribersPath = join(__dirname, '..', 'subscribers', '*.subscriber.{ts,js}');
export const connectionConfig = {
name: 'default',
type: 'postgres',
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_DATABASE,
schema: process.env.DB_SCHEMA,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
};
export const databaseConfig = registerAs('database', () => ({
entities: [entitiesPath],
migrations: [migrationsPath],
subscribers: [subscribersPath],
...connectionConfig,
}));
database.module.ts
:
const connectionProvider = {
provide: DatabaseProvider.Connection,
useFactory: async (configService: ConfigService): Promise<Connection> => {
const databaseConfig = configService.get('database');
return await createConnection({
...databaseConfig,
});
},
inject: [ConfigService],
};
@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig],
}),
],
providers: [connectionProvider, TestRepository],
exports: [connectionProvider, TestRepository],
})
export class DatabaseModule {}
TestRepository
is a class that extends a BaseRepository
with a unit of work almost like described here.
The connection is injected in it like this:
constructor(@Inject(DatabaseProvider.Connection) private readonly conn: Connection) {
super(conn);
}
In the base repository I create the QueryRunner
in the constructor:
constructor(connection: Connection) {
super();
this.connection = connection;
this.unitOfWorkQueryRunner = this.connection.createQueryRunner();
}
Now, I want to write some integration test for the unit of work and I am getting the connection and TestRepository
like this:
describe('test.repository.ts', () => {
let app: INestApplication;
let connection: Connection;
let testRepository: TestRepository;
beforeAll(async () => {
app = await NestFactory.create(DatabaseModule);
connection = app.get<Connection>(DatabaseProvider.Connection);
});
beforeEach(async () => {
await connection.runMigrations();
testRepository = connection.getCustomRepository(TestRepository);
});
[...]
It seems like the testRepository
and connection
are initialized correctly by on the line this.unitOfWorkQueryRunner = this.connection.createQueryRunner()
I get the error createQueryRunner
is not a function.
What am I doing wrong?
EDIT:
connection
:
<ref *1> Connection {
migrations: [
CreateBrandTable1628717011030 {
name: 'CreateBrandTable1628717011030'
}
],
subscribers: [ GlobalSubscriber {} ],
entityMetadatas: [
EntityMetadata {
childEntityMetadatas: [],
inheritanceTree: [Array],
tableType: 'regular',
withoutRowid: false,
synchronize: true,
hasNonNullableRelations: false,
isJunction: false,
isAlwaysUsingConstructor: true,
isClosureJunction: false,
hasMultiplePrimaryKeys: false,
hasUUIDGeneratedColumns: true,
ownColumns: [Array],
columns: [Array],
ancestorColumns: [],
descendantColumns: [],
nonVirtualColumns: [Array],
ownerColumns: [],
inverseColumns: [],
generatedColumns: [Array],
primaryColumns: [Array],
ownRelations: [],
relations: [],
eagerRelations: [],
lazyRelations: [],
oneToOneRelations: [],
ownerOneToOneRelations: [],
oneToManyRelations: [],
manyToOneRelations: [],
manyToManyRelations: [],
ownerManyToManyRelations: [],
relationsWithJoinColumns: [],
relationIds: [],
relationCounts: [],
foreignKeys: [],
embeddeds: [],
allEmbeddeds: [],
ownIndices: [],
indices: [],
uniques: [],
ownUniques: [],
checks: [],
exclusions: [],
ownListeners: [],
listeners: [],
afterLoadListeners: [],
beforeInsertListeners: [],
afterInsertListeners: [],
beforeUpdateListeners: [],
afterUpdateListeners: [],
beforeRemoveListeners: [],
afterRemoveListeners: [],
connection: [Circular *1],
inheritancePattern: undefined,
treeType: undefined,
treeOptions: undefined,
parentClosureEntityMetadata: undefined,
tableMetadataArgs: [Object],
target: [class Brand extends CustomBaseEntity],
expression: undefined,
engine: undefined,
database: undefined,
schema: 'sh',
givenTableName: undefined,
targetName: 'Brand',
tableNameWithoutPrefix: 'brand',
tableName: 'brand',
name: 'Brand',
tablePath: 'sh.brand',
orderBy: undefined,
discriminatorValue: 'Brand',
treeParentRelation: undefined,
treeChildrenRelation: undefined,
createDateColumn: [ColumnMetadata],
updateDateColumn: undefined,
deleteDateColumn: undefined,
versionColumn: undefined,
discriminatorColumn: undefined,
treeLevelColumn: undefined,
nestedSetLeftColumn: undefined,
nestedSetRightColumn: undefined,
materializedPathColumn: undefined,
objectIdColumn: undefined,
propertiesMap: [Object]
}
],
name: 'default',
options: {
entities: [
...
],
migrations: [
...
],
subscribers: [
...
],
name: 'default',
type: 'postgres',
host: 'localhost',
port: '5432',
database: 'database_name',
schema: 'sh',
username: 'sh_user',
password: 'password'
},
logger: AdvancedConsoleLogger { options: undefined },
driver: PostgresDriver {
slaves: [],
connectedQueryRunners: [],
isReplicated: false,
treeSupport: true,
supportedDataTypes: [
'int',
'int2',
'int4',
'int8',
'smallint',
'integer',
'bigint',
'decimal',
'numeric',
'real',
'float',
'float4',
'float8',
'double precision',
'money',
'character varying',
'varchar',
'character',
'char',
'text',
'citext',
'hstore',
'bytea',
'bit',
'varbit',
'bit varying',
'timetz',
'timestamptz',
'timestamp',
'timestamp without time zone',
'timestamp with time zone',
'date',
'time',
'time without time zone',
'time with time zone',
'interval',
'bool',
'boolean',
'enum',
'point',
'line',
'lseg',
'box',
'path',
'polygon',
'circle',
'cidr',
'inet',
'macaddr',
'tsvector',
'tsquery',
'uuid',
'xml',
'json',
'jsonb',
'int4range',
'int8range',
'numrange',
'tsrange',
'tstzrange',
'daterange',
'geometry',
'geography',
'cube',
'ltree'
],
spatialTypes: [ 'geometry', 'geography' ],
withLengthColumnTypes: [
'character varying',
'varchar',
'character',
'char',
'bit',
'varbit',
'bit varying'
],
withPrecisionColumnTypes: [
'numeric',
'decimal',
'interval',
'time without time zone',
'time with time zone',
'timestamp without time zone',
'timestamp with time zone'
],
withScaleColumnTypes: [ 'numeric', 'decimal' ],
mappedDataTypes: {
createDate: 'timestamp',
createDateDefault: 'now()',
updateDate: 'timestamp',
updateDateDefault: 'now()',
deleteDate: 'timestamp',
deleteDateNullable: true,
version: 'int4',
treeLevel: 'int4',
migrationId: 'int4',
migrationName: 'varchar',
migrationTimestamp: 'int8',
cacheId: 'int4',
cacheIdentifier: 'varchar',
cacheTime: 'int8',
cacheDuration: 'int4',
cacheQuery: 'text',
cacheResult: 'text',
metadataType: 'varchar',
metadataDatabase: 'varchar',
metadataSchema: 'varchar',
metadataTable: 'varchar',
metadataName: 'varchar',
metadataValue: 'text'
},
dataTypeDefaults: {
character: [Object],
bit: [Object],
interval: [Object],
'time without time zone': [Object],
'time with time zone': [Object],
'timestamp without time zone': [Object],
'timestamp with time zone': [Object]
},
maxAliasLength: 63,
connection: [Circular *1],
options: {
entities: [Array],
migrations: [Array],
subscribers: [Array],
name: 'default',
type: 'postgres',
host: 'localhost',
port: '5432',
database: 'database_name',
schema: 'sh',
username: 'sh_user',
password: 'password'
},
postgres: PG {
defaults: [Object],
Client: [Function],
Query: [class Query extends EventEmitter],
Pool: [class BoundPool extends Pool],
_pools: [],
Connection: [class Connection extends EventEmitter],
types: [Object],
DatabaseError: [class DatabaseError extends Error]
},
database: 'competitor_monitoring_tool_test',
master: BoundPool {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
options: [Object],
log: [Function (anonymous)],
Client: [Function],
Promise: [Function: Promise],
_clients: [Array],
_idle: [Array],
_pendingQueue: [],
_endCallback: undefined,
ending: false,
ended: false,
[Symbol(kCapture)]: false
}
},
manager: EntityManager {
repositories: [],
plainObjectToEntityTransformer: PlainObjectToNewEntityTransformer {},
connection: [Circular *1]
},
namingStrategy: DefaultNamingStrategy {
nestedSetColumnNames: { left: 'nsleft', right: 'nsright' },
materializedPathColumnName: 'mpath'
},
queryResultCache: undefined,
relationLoader: RelationLoader { connection: [Circular *1] },
isConnected: true
}
Repo to reproduce a similar issue (probably it's the same config issue): https://github.com/y-chen/nestjs-typeorm-undefined-issue
Upvotes: 0
Views: 5789
Reputation: 84
It is not necessary to inject the connection to a repository that extends the Repository
/AbstractRepository
from TypeORM, since you can access it using this.manager.connection
like this:
this.manager.connection.createQueryRunner();
However, you can't access the connection in the constructor, since the manager (and as such the connection) is injected to the repository after it's instantiated. In your case, you only need the QueryRunner
after calling BaseRepository#begin()
, so you can update your repository to something like this:
export default abstract class BaseRepository<T extends CustomBaseEntity> extends Repository<T> {
private unitOfWorkQueryRunner?: QueryRunner;
setTransactionManager(): void {
this.unitOfWorkQueryRunner = this.manager.connection.createQueryRunner();
}
async begin(): Promise<void> {
this.setTransactionManager();
await this.unitOfWorkQueryRunner.startTransaction();
}
// ...
async write(...entities: T[]): Promise<T | T[]> {
return await this.unitOfWorkQueryRunner.manager.save(entities);
}
// ...
}
Edit: After looking the repo you linked it seems you don't use the @nestjs/typeorm package. You should try it, as it simplifies the setup a lot. For more info check this page from the NestJS documentation. But as a summary:
Add this to the @Module()
decorator in AppModule
(using TypeOrmModule.forRootAsync()
to get access to ConfigService
like in your repo):
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'sqlite',
database: './data/test.db',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
}),
inject: [ConfigService],
}),
BrandModule, // see below
],
// providers, exports, etc
})
export class AppModule {}
Add this to the @Module()
decorator where you want to use a repository (you can add all repositories to AppModule
, but it is a good practice to split it up in multiple Feature Modules):
@Module({
imports: [
TypeOrmModule.forFeature([Brand]), // Brand is an entity
],
providers: [BrandService],
exports: [BrandService],
})
export class BrandModule {}
Inject the repository in a service like this:
@Injectable()
export class BrandService {
constructor(
@InjectRepository(Brand)
private brandRepository: Repository<Brand>,
) {}
}
Upvotes: 1