chenny
chenny

Reputation: 941

typeorm connection.createQueryRunner is not a function when used in nestjs

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

Answers (1)

Mat&#237;as Pizarro
Mat&#237;as Pizarro

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

Related Questions