Skip to content
This repository has been archived by the owner on Feb 2, 2018. It is now read-only.

Commit

Permalink
feature(main): Basic mixin implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Kevin Delisle committed Nov 22, 2017
1 parent 6ff849a commit 885e6de
Show file tree
Hide file tree
Showing 9 changed files with 627 additions and 26 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
# loopback-typeorm
A component to provide TypeORM in Loopback 4

## Usage
Note: These instructions aren't entirely applicable yet, this module has not
been published.

1. Install this plugin
```ts
npm install --save loopback-typeorm
```
2. In your application, make your own Application class, but instead of
extending `Application`, you'll want to call the provided mixin as your base
class.
```ts
import {Application} from '@loopback/core';
import {TypeORMRepositoryMixin} from 'loopback-typeorm';

export class MyApplication extends TypeORMRepositoryMixin(Application) {
constructor() {
super(...);
}
}
```
3. Ensure that the superclass constructor (the one for the mixin) is provided
connection info required for your database.
A helpful way to ensure that your configuration has all of the _required_ values
is to import the `ConnectionOptions` type from TypeORM directly.

**Note**: There are connection options that become required within different
use cases and contexts. For info on how to configure your database connection,
see the [TypeORM docs](https://github.com/typeorm/typeorm).

```ts
import {Application} from '@loopback/core';
import {TypeORMRepositoryMixin} from 'loopback-typeorm';
import {ConnectionOptions} from 'typeorm';

export class MyApplication extends TypeORMRepositoryMixin(Application) {
constructor() {
const connectionOptions: ConnectionOptions = {
name: 'connectionName',
host: 'somehost.com',
database: 'mydb',
port: 3306,
type: 'mysql',
username: 'admin',
password: 'secretpassword',
// etc...
};
super();
}
}
}
```

## Testing
To run tests, you'll need an installation of Docker.
```
# This will pull and setup a mysql instance to run acceptance tests against.
source setup.sh
# This runs install and test one after the other.
npm it
```

[![LoopBack](http://loopback.io/images/overview/powered-by-LB-xs.png)](http://loopback.io/)

10 changes: 0 additions & 10 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: loopback-typeorm
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

// NOTE(bajtos) This file is used by TypeScript compiler to resolve imports
// from "test" files against original TypeScript sources in "src" directory.
// As a side effect, `tsc` also produces "dist/index.{js,d.ts,map} files
// that allow test files to import paths pointing to {src,test} root directory,
// which is project root for TS sources but "dist" for transpiled sources.
export * from './src';
29 changes: 23 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"name": "loopback-typeorm",
"version": "0.0.1",
"description": "A component to provide TypeORM in Loopback 4",
"keywords": ["loopback-application", "loopback"],
"keywords": [
"loopback-application",
"loopback"
],
"main": "index.js",
"engines": {
"node": ">=8"
Expand All @@ -16,28 +19,42 @@
"prettier:cli": "prettier \"**/*.ts\" \"**/*.js\"",
"prettier:check": "npm run prettier:cli -- -l",
"prettier:fix": "npm run prettier:cli -- --write",
"tslint": "tslint",
"tslint": "tslint .",
"tslint:fix": "npm run tslint -- --fix",
"pretest": "npm run clean && npm run build",
"posttest": "npm run lint",
"start": "npm run build && node .",
"prepare": "npm run build"
"prepare": "npm run build",
"test": "npm run build && node ./dist/test/setup.js"
},
"repository": {
"type": "git"
},
"author": "",
"license": "MIT",
"files": ["README.md", "index.js", "index.d.ts", "dist"],
"files": [
"README.md",
"index.js",
"index.d.ts",
"dist"
],
"dependencies": {
"@loopback/context": "^4.0.0-alpha.18",
"@loopback/core": "^4.0.0-alpha.20",
"@loopback/rest": "^4.0.0-alpha.7"
"typeorm": "^0.1.2"
},
"devDependencies": {
"@loopback/build": "^4.0.0-alpha.5",
"@loopback/testlab": "^4.0.0-alpha.14",
"@types/debug": "0.0.30",
"@types/dockerode": "^2.5.1",
"@types/mocha": "^2.2.44",
"debug": "^3.1.0",
"dockerode": "^2.5.3",
"mocha": "^4.0.1",
"mysql": "^2.15.0",
"prettier": "^1.7.3",
"tslint": "^5.7.0",
"@loopback/testlab": "^4.0.0-alpha.13",
"typescript": "^2.5.3"
}
}
7 changes: 7 additions & 0 deletions src/connection-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {ConnectionManager, Connection} from 'typeorm';

export class TypeORMConnectionManager extends ConnectionManager {
// This is to allow more direct access to the connection objects
// during start/stop of the application.
public connections: Connection[];
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './typeorm-mixin';
103 changes: 103 additions & 0 deletions src/typeorm-mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {Application, Component, Server} from '@loopback/core';
import {Context, Binding, Constructor} from '@loopback/context';
import {Connection, Entity, BaseEntity, ConnectionOptions} from 'typeorm';
import {TypeORMConnectionManager} from './connection-manager';

// tslint:disable:no-any
export function TypeORMMixin(
superClass: typeof Application,
): TypeORMApplicationClass {
return class extends superClass {
typeOrmConnectionManager: TypeORMConnectionManager;
constructor(...args: any[]) {
super(...args);
this.typeOrmConnectionManager = new TypeORMConnectionManager();
this.bind('typeorm.connections.manager').to(
this.typeOrmConnectionManager,
);
}

async start() {
for (const connection of this.typeOrmConnectionManager.connections) {
await connection.connect();
}
await super.start();
}

async stop() {
for (const connection of this.typeOrmConnectionManager.connections) {
await connection.close();
}
await super.stop();
}

/**
* Register a TypeORM-based repository instance of the given class.
* Generated repositories will be bound using the `repositories.{name}`
* convention.
*
* ```ts
* this.typeOrmRepository(Foo);
* const fooRepo = this.getSync(`repositories.Foo`);
* ```
*
* @param ctor The constructor (class) that represents the entity to
* generate a repository for.
*/
typeOrmRepository<S>(
connection: Connection,
ctor: Constructor<S>,
): Binding {
// XXX(kjdelisle): I wanted to make this a provider, but it requires
// the constructor instance to be available in the provider scope, which
// would require injection of each constructor, so I had to settle for
// this instead.
return this.bind(`repositories.${ctor.name}`).toDynamicValue(async () => {
if (!connection.isConnected) {
await connection.connect();
}
return connection.getRepository(ctor);
});
}

/**
* Get an existing connection instance from the connection manager,
* or create one if it does not exist. If you do not provide a name, a
* default connection instance will be provided.
* @param name The name of the connection (if it already exists)
*/
getTypeOrmConnection(name?: string): Connection {
return this.typeOrmConnectionManager.get(name);
}

/**
* Create a new TypeORM connection with the provided set of options.
* @param options
*/
createTypeOrmConnection(options: ConnectionOptions): Connection {
if (!options) {
throw new Error('Connection options are required!');
}
return this.typeOrmConnectionManager.create(options);
}
};
}

/**
* Define any implementation of Application.
*/

export interface TypeORMApplication extends Application {
typeOrmRepository<S>(connection: Connection, ctor: Constructor<S>): Binding;
createTypeOrmConnection(options: ConnectionOptions): Connection;
getTypeOrmConnection(name?: string): Connection;
}

export interface TypeORMApplicationClass
extends Constructor<TypeORMApplication> {
[property: string]: any;
}

export interface Options {
[property: string]: any;
}
144 changes: 144 additions & 0 deletions test/acceptance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import 'mocha';
import {expect} from '@loopback/testlab';
import {Application} from '@loopback/core';
import {TypeORMMixin} from '../src/index';
import {
Repository,
ConnectionOptions,
Entity,
PrimaryGeneratedColumn,
Column,
Connection,
OneToMany,
ManyToOne,
JoinTable,
} from 'typeorm';
import * as util from 'util';

/*
* ============================================================================
* FIXTURES
* ============================================================================
*/
class TestApplication extends TypeORMMixin(Application) {
connectionOne: Connection;
connectionTwo: Connection;
constructor() {
super();
const fakeConnectionInfo: ConnectionOptions = {
host: process.env.MYSQL_HOST || 'localhost',
database: process.env.MYSQL_DATABASE || 'testdb',
port: Number.parseInt(process.env.MYSQL_PORT || '3306'),
type: 'mysql',
username: process.env.MYSQL_USERNAME || 'root',
password: process.env.MYSQL_PASSWORD || 'pass',
entities: [Customer, Order],
synchronize: true,
};
console.log(
`Connection Info: ${util.inspect(fakeConnectionInfo, undefined, 2)}`,
);
this.connectionOne = this.createTypeOrmConnection(
Object.assign({name: 'one'}, fakeConnectionInfo),
);
this.connectionTwo = this.createTypeOrmConnection(
Object.assign({name: 'two'}, fakeConnectionInfo),
);

this.typeOrmRepository(this.connectionOne, Order);
this.typeOrmRepository(this.connectionTwo, Customer);
}
}

@Entity()
class Order {
@PrimaryGeneratedColumn() id: number;
@Column() orderDate: Date;
@ManyToOne(type => Customer, customer => customer.orders)
customer: number;
@Column({type: 'int', nullable: true})
customerId: number;
}

@Entity()
class Customer {
@PrimaryGeneratedColumn() id: number;
@Column() name?: string;
@Column() address?: string;
@OneToMany(type => Order, order => order.customer)
@JoinTable()
orders: Order[];
}

/*
* ============================================================================
* TESTS
* ============================================================================
*/

describe('TypeORM Repository Mixin', () => {
const app = new TestApplication();
before(async () => {
await app.start();
});

it('creates repository bindings', async () => {
expect(await app.get(`repositories.Order`)).to.be.instanceof(Repository);
expect(await app.get(`repositories.Customer`)).to.be.instanceof(Repository);
});

describe('operations', () => {
// NOTE: This is not meant to be a fully functional set of CRUD tests.
// TypeORM has its own suite of tests.
it('can create entities', async () => {
const customer = getCustomer();
const repo = (await app.get(
`repositories.${Customer.name}`,
)) as Repository<Customer>;
const foo = await repo.save(customer);
const result = await repo.findOneById(foo.id);
expect(result).to.deepEqual(foo);
});

it('can run more advanced queries', async () => {
let customer = getCustomer();
const customerRepo = (await app.get(
'repositories.Customer',
)) as Repository<Customer>;
customer = await customerRepo.save(customer);
let order = getOrder(customer.id);
const orderRepo = (await app.get('repositories.Order')) as Repository<
Order
>;
order = await orderRepo.save(order);

let result = (await customerRepo
.createQueryBuilder('customer')
.innerJoinAndSelect('customer.orders', 'orders')
.getOne()) as Customer;

expect(result).to.containDeep(customer);
expect(result.orders[0]).to.containDeep(order);
});
});

after(async () => {
await app.stop();
});
});

function getCustomer(customer?: Partial<Customer>): Customer {
const base = new Customer();
base.id = 0;
base.name = 'someName';
base.address = '123 Fake St.';
return Object.assign(base, customer);
}

function getOrder(customerId: number, order?: Partial<Order>): Order {
const base = new Order();
base.id = 0;
base.orderDate = new Date('2012-12-25T00:00:00.000Z');
base.customerId = customerId;
return Object.assign(base, order);
}
Loading

0 comments on commit 885e6de

Please sign in to comment.