From b95c77f3c664adccf808557f7e90d73b0abbced6 Mon Sep 17 00:00:00 2001 From: Will Soto Date: Fri, 10 Jul 2020 08:43:34 -0400 Subject: [PATCH] feat(*): support multiple connections Closes #684 Signed-off-by: Will Soto --- lib/core.ts | 28 +++-- lib/interfaces.ts | 12 +- tests/core.spec.ts | 15 ++- tests/module.spec.ts | 5 +- tests/multiple-connections.spec.ts | 170 +++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 tests/multiple-connections.spec.ts diff --git a/lib/core.ts b/lib/core.ts index da1f748c..1febb489 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -1,5 +1,11 @@ /* eslint-disable new-cap */ -import { DynamicModule, Module, Provider } from "@nestjs/common"; +import { + DynamicModule, + FactoryProvider, + Module, + Provider, + ValueProvider, +} from "@nestjs/common"; import Knex from "knex"; import { Model } from "objection"; import { @@ -22,18 +28,18 @@ export class ObjectionCoreModule { BaseModel.knex(connection); - const objectionModuleOptions: Provider = { + const objectionModuleOptions: ValueProvider = { provide: OBJECTION_MODULE_OPTIONS, useValue: options, }; - const objectionBaseModelProvider: Provider = { - provide: OBJECTION_BASE_MODEL, + const objectionBaseModelProvider: ValueProvider = { + provide: BaseModel.name, useValue: BaseModel, }; - const knexConnectionProvider: Provider = { - provide: KNEX_CONNECTION, + const knexConnectionProvider: ValueProvider = { + provide: options.name || KNEX_CONNECTION, useValue: connection, }; @@ -51,17 +57,19 @@ export class ObjectionCoreModule { public static registerAsync( options: ObjectionModuleAsyncOptions = {}, ): DynamicModule { - const knexConnectionProvider: Provider = { - provide: KNEX_CONNECTION, + const connectionToken = options.name || KNEX_CONNECTION; + + const knexConnectionProvider: FactoryProvider = { + provide: connectionToken, inject: [OBJECTION_MODULE_OPTIONS], useFactory(objectionModuleOptions: ObjectionModuleOptions): Knex { return Knex(objectionModuleOptions.config); }, }; - const objectionBaseModelProvider: Provider = { + const objectionBaseModelProvider: FactoryProvider = { provide: OBJECTION_BASE_MODEL, - inject: [KNEX_CONNECTION, OBJECTION_MODULE_OPTIONS], + inject: [connectionToken, OBJECTION_MODULE_OPTIONS], useFactory( connection: Connection, objectionModuleOptions: ObjectionModuleOptions, diff --git a/lib/interfaces.ts b/lib/interfaces.ts index ad42e07f..6690eaff 100644 --- a/lib/interfaces.ts +++ b/lib/interfaces.ts @@ -1,9 +1,14 @@ import { Type } from "@nestjs/common"; import { ModuleMetadata } from "@nestjs/common/interfaces"; -import * as Knex from "knex"; +import Knex from "knex"; import { Model } from "objection"; export interface ObjectionModuleOptions { + /** + * The name for this connection if more than one database connection is required. + * This field is **required** if you use multiple connetions. + */ + name?: string; Model?: typeof Model; config: Knex.Config; } @@ -16,6 +21,11 @@ export interface ObjectionModuleOptionsFactory { export interface ObjectionModuleAsyncOptions extends Pick { + /** + * The name for this connection if more than one database connection is required. + * This field is **required** if you use multiple connetions. + */ + name?: ObjectionModuleOptions["name"]; useExisting?: Type; useClass?: Type; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/tests/core.spec.ts b/tests/core.spec.ts index df4d4bb7..2fcde8df 100644 --- a/tests/core.spec.ts +++ b/tests/core.spec.ts @@ -1,6 +1,7 @@ import { Injectable } from "@nestjs/common"; import { Test, TestingModule } from "@nestjs/testing"; -import knex from "knex"; +import { Config } from "knex"; +import { Model } from "objection"; import { KNEX_CONNECTION, OBJECTION_BASE_MODEL, @@ -14,7 +15,7 @@ import { describe("ObjectionCoreModule", () => { let testingModule: TestingModule; - const config: knex.Config = { + const config: Config = { client: "sqlite3", useNullAsDefault: true, connection: { @@ -40,14 +41,14 @@ describe("ObjectionCoreModule", () => { }); test("provides a base model", () => { - const model = testingModule.get("ObjectionBaseModel"); + const model = testingModule.get(Model); - expect(model).toBeDefined(); + expect(model).toBe(Model); }); }); describe("#registerAsync", () => { - beforeEach(async () => { + beforeAll(async () => { testingModule = await Test.createTestingModule({ imports: [ ObjectionCoreModule.registerAsync({ @@ -61,6 +62,8 @@ describe("ObjectionCoreModule", () => { }).compile(); }); + afterAll(() => testingModule.close()); + test("provides a connection", () => { const connection = testingModule.get(KNEX_CONNECTION); @@ -70,7 +73,7 @@ describe("ObjectionCoreModule", () => { test("provides a base model", () => { const model = testingModule.get(OBJECTION_BASE_MODEL); - expect(model).toBeDefined(); + expect(model).toBe(Model); }); }); diff --git a/tests/module.spec.ts b/tests/module.spec.ts index 8661ebca..7ac2476d 100644 --- a/tests/module.spec.ts +++ b/tests/module.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from "@nestjs/testing"; import knex from "knex"; -import { KNEX_CONNECTION, OBJECTION_BASE_MODEL } from "../lib/constants"; +import { Model } from "objection"; +import { KNEX_CONNECTION } from "../lib/constants"; import { ObjectionModule } from "../lib/module"; import { User } from "./fixtures"; @@ -32,7 +33,7 @@ describe("ObjectionModule", () => { }); test("provides a base model", () => { - const model = testingModule.get(OBJECTION_BASE_MODEL); + const model = testingModule.get(Model); expect(model).toBeDefined(); }); diff --git a/tests/multiple-connections.spec.ts b/tests/multiple-connections.spec.ts new file mode 100644 index 00000000..19b57031 --- /dev/null +++ b/tests/multiple-connections.spec.ts @@ -0,0 +1,170 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import Knex from "knex"; +import { Model } from "objection"; +import { ObjectionCoreModule } from "../lib/core"; + +describe("when registering multiple connections", () => { + let testingModule: TestingModule; + let connection1: Knex; + let connection2: Knex; + + class Author extends Model { + static tableName = "authors"; + } + + class Book extends Model { + static tableName = "books"; + } + + describe("#register", () => { + beforeAll(async () => { + testingModule = await Test.createTestingModule({ + imports: [ + ObjectionCoreModule.register({ + name: "connection1", + Model: Author, + config: { + client: "sqlite3", + useNullAsDefault: true, + connection: { + filename: "./testing.sqlite", + }, + }, + }), + ObjectionCoreModule.register({ + name: "connection2", + Model: Book, + config: { + client: "sqlite3", + useNullAsDefault: true, + connection: { + filename: "./testing2.sqlite", + }, + }, + }), + ], + }).compile(); + + connection1 = testingModule.get("connection1"); + connection2 = testingModule.get("connection2"); + + if (!(await connection1.schema.hasTable(Author.tableName))) { + await connection1.schema.createTable(Author.tableName, (t) => { + t.increments().primary().notNullable(); + t.text("name").notNullable(); + }); + } + + if (!(await connection2.schema.hasTable(Book.tableName))) { + await connection2.schema.createTable(Book.tableName, (t) => { + t.increments().primary().notNullable(); + t.text("title").notNullable(); + }); + } + }); + + afterAll(() => testingModule.close()); + + test("uses the given token for each connection", () => { + expect(connection1).toBeDefined(); + expect(connection2).toBeDefined(); + }); + + test("queries using the correct connection", async () => { + expect(Author.knex()).toEqual(connection1); + expect(Book.knex()).toEqual(connection2); + + await expect(Author.query()).resolves.toEqual([]); + await expect(Book.query()).resolves.toEqual([]); + }); + + test("created tables", async () => { + await expect(connection1.schema.hasTable(Book.tableName)).resolves.toBe( + false, + ); + await expect(connection2.schema.hasTable(Author.tableName)).resolves.toBe( + false, + ); + }); + }); + + describe("#registerAsync", () => { + beforeAll(async () => { + testingModule = await Test.createTestingModule({ + imports: [ + ObjectionCoreModule.registerAsync({ + name: "connection1", + useFactory() { + return { + Model: Author, + config: { + client: "sqlite3", + useNullAsDefault: true, + connection: { + filename: "./testing.sqlite", + }, + }, + }; + }, + }), + ObjectionCoreModule.registerAsync({ + name: "connection2", + useFactory() { + return { + Model: Book, + config: { + client: "sqlite3", + useNullAsDefault: true, + connection: { + filename: "./testing2.sqlite", + }, + }, + }; + }, + }), + ], + }).compile(); + + connection1 = testingModule.get("connection1"); + connection2 = testingModule.get("connection2"); + + if (!(await connection1.schema.hasTable(Author.tableName))) { + await connection1.schema.createTable(Author.tableName, (t) => { + t.increments().primary().notNullable(); + t.text("name").notNullable(); + }); + } + + if (!(await connection2.schema.hasTable(Book.tableName))) { + await connection2.schema.createTable(Book.tableName, (t) => { + t.increments().primary().notNullable(); + t.text("title").notNullable(); + }); + } + }); + + afterAll(() => testingModule.close()); + + test("uses the given token for each connection", () => { + expect(connection1).toBeDefined(); + expect(connection2).toBeDefined(); + }); + + test("queries using the correct connection", async () => { + expect(Author.knex()).toEqual(connection1); + expect(Book.knex()).toEqual(connection2); + + await expect(Author.query()).resolves.toEqual([]); + await expect(Book.query()).resolves.toEqual([]); + }); + + test("created tables", async () => { + await expect(connection1.schema.hasTable(Book.tableName)).resolves.toBe( + false, + ); + await expect(connection2.schema.hasTable(Author.tableName)).resolves.toBe( + false, + ); + }); + }); +});