diff --git a/acceptance/repository-mongodb/package.json b/acceptance/repository-mongodb/package.json index 26b1b2e81649..43159d4aa0c9 100644 --- a/acceptance/repository-mongodb/package.json +++ b/acceptance/repository-mongodb/package.json @@ -24,6 +24,7 @@ "@loopback/eslint-config": "^9.0.0", "@loopback/repository": "^2.11.0", "@loopback/repository-tests": "^0.12.11", + "@loopback/testlab": "^3.2.2", "@types/node": "^10.17.28", "loopback-connector-mongodb": "^5.3.0", "tslib": "^2.0.1" diff --git a/acceptance/repository-mongodb/src/__tests__/mongodb-repository-execute.acceptance.ts b/acceptance/repository-mongodb/src/__tests__/mongodb-repository-execute.acceptance.ts new file mode 100644 index 000000000000..e6590ffea876 --- /dev/null +++ b/acceptance/repository-mongodb/src/__tests__/mongodb-repository-execute.acceptance.ts @@ -0,0 +1,63 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/test-repository-mongodb +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + DefaultTransactionalRepository, + Entity, + juggler, + model, + property, +} from '@loopback/repository'; +import {expect, toJSON} from '@loopback/testlab'; +import {MONGODB_CONFIG} from './mongodb.datasource'; + +describe('MongoDB repository.execute()', () => { + it('executes a custom MongoDB query', async () => { + const db = new juggler.DataSource(MONGODB_CONFIG); + + @model() + class Product extends Entity { + @property({id: true, generated: true}) + id: string; + + @property({required: true}) + name: string; + + @property() + owner: string; + } + + const repo = new DefaultTransactionalRepository( + Product, + db, + ); + await db.automigrate(Product.modelName); + await repo.create({name: 'Pen', owner: 'bajtos'}); + await repo.create({name: 'Car', owner: 'admin'}); + await repo.create({name: 'Chair', owner: 'admin'}); + + // MongoDB's aggregate() command returns an AggregationCursor instance + const cursor = await repo.execute('Product', 'aggregate', [ + { + $group: { + _id: '$owner', + count: {$sum: 1}, + }, + }, + { + $sort: { + _id: 1, + }, + }, + ]); + // Fetch all items from the cursor at once + const result = await cursor.toArray(); + + expect(toJSON(result)).to.deepEqual([ + {_id: 'admin', count: 2}, + {_id: 'bajtos', count: 1}, + ]); + }); +}); diff --git a/acceptance/repository-mongodb/tsconfig.json b/acceptance/repository-mongodb/tsconfig.json index 771d414bef7f..1c7832af7d75 100644 --- a/acceptance/repository-mongodb/tsconfig.json +++ b/acceptance/repository-mongodb/tsconfig.json @@ -15,6 +15,9 @@ }, { "path": "../../packages/repository/tsconfig.json" + }, + { + "path": "../../packages/testlab/tsconfig.json" } ] } diff --git a/acceptance/repository-mysql/package.json b/acceptance/repository-mysql/package.json index 92d29ca8ce16..6770f7fcaf9b 100644 --- a/acceptance/repository-mysql/package.json +++ b/acceptance/repository-mysql/package.json @@ -24,6 +24,7 @@ "@loopback/eslint-config": "^9.0.0", "@loopback/repository": "^2.11.0", "@loopback/repository-tests": "^0.12.11", + "@loopback/testlab": "^3.2.2", "@types/node": "^10.17.28", "loopback-connector-mysql": "^5.4.4", "tslib": "^2.0.1" diff --git a/acceptance/repository-mysql/src/__tests__/mysql-repository-execute.acceptance.ts b/acceptance/repository-mysql/src/__tests__/mysql-repository-execute.acceptance.ts new file mode 100644 index 000000000000..8555d40b6cd0 --- /dev/null +++ b/acceptance/repository-mysql/src/__tests__/mysql-repository-execute.acceptance.ts @@ -0,0 +1,43 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/test-repository-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + DefaultTransactionalRepository, + Entity, + juggler, + model, + property, +} from '@loopback/repository'; +import {expect, toJSON} from '@loopback/testlab'; +import {MYSQL_CONFIG} from './mysql.datasource'; + +describe('MySQL repository.execute()', () => { + it('executes a parameterized native SQL query', async () => { + const db = new juggler.DataSource(MYSQL_CONFIG); + + @model() + class Product extends Entity { + @property({id: true, generated: true}) + id: number; + + @property({required: true}) + name: string; + } + + const repo = new DefaultTransactionalRepository( + Product, + db, + ); + await db.automigrate(Product.modelName); + const pen = await repo.create({name: 'Pen'}); + await repo.create({name: 'Car'}); + + const result = await repo.execute('SELECT * FROM Product WHERE name = ?', [ + 'Pen', + ]); + + expect(toJSON(result)).to.deepEqual([toJSON(pen)]); + }); +}); diff --git a/acceptance/repository-mysql/tsconfig.json b/acceptance/repository-mysql/tsconfig.json index 771d414bef7f..1c7832af7d75 100644 --- a/acceptance/repository-mysql/tsconfig.json +++ b/acceptance/repository-mysql/tsconfig.json @@ -15,6 +15,9 @@ }, { "path": "../../packages/repository/tsconfig.json" + }, + { + "path": "../../packages/testlab/tsconfig.json" } ] } diff --git a/acceptance/repository-postgresql/package.json b/acceptance/repository-postgresql/package.json index 1cb0f34f6984..45184b057d97 100644 --- a/acceptance/repository-postgresql/package.json +++ b/acceptance/repository-postgresql/package.json @@ -24,6 +24,7 @@ "@loopback/eslint-config": "^9.0.0", "@loopback/repository": "^2.11.0", "@loopback/repository-tests": "^0.12.11", + "@loopback/testlab": "^3.2.2", "@types/node": "^10.17.28", "loopback-connector-postgresql": "^5.0.2", "tslib": "^2.0.1" diff --git a/acceptance/repository-postgresql/setup.sh b/acceptance/repository-postgresql/setup.sh index a42fc351e7b1..982fbbb0cd5c 100644 --- a/acceptance/repository-postgresql/setup.sh +++ b/acceptance/repository-postgresql/setup.sh @@ -1 +1,2 @@ source ./node_modules/loopback-connector-postgresql/setup.sh +export POSTGRESQL_DATABASE=repository_tests diff --git a/acceptance/repository-postgresql/src/__tests__/postgresql-repository-execute.acceptance.ts b/acceptance/repository-postgresql/src/__tests__/postgresql-repository-execute.acceptance.ts new file mode 100644 index 000000000000..777e40bf5182 --- /dev/null +++ b/acceptance/repository-postgresql/src/__tests__/postgresql-repository-execute.acceptance.ts @@ -0,0 +1,45 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/test-repository-postgresql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + DefaultTransactionalRepository, + Entity, + juggler, + model, + property, +} from '@loopback/repository'; +import {expect, toJSON} from '@loopback/testlab'; +import {POSTGRESQL_CONFIG} from './postgresql.datasource'; + +describe('PostgreSQL repository.execute()', () => { + // FIXME(bajtos) This test passes when executed in isolation, but fails + // on connection timeout when executed after repository-test suite. + it.skip('executes a parameterized native SQL query', async () => { + const db = new juggler.DataSource(POSTGRESQL_CONFIG); + + @model() + class Product extends Entity { + @property({id: true, generated: true}) + id: number; + + @property({required: true}) + name: string; + } + + const repo = new DefaultTransactionalRepository( + Product, + db, + ); + await db.automigrate(Product.modelName); + const pen = await repo.create({name: 'Pen'}); + await repo.create({name: 'Car'}); + + const result = await repo.execute('SELECT * FROM Product WHERE name = $1', [ + 'Pen', + ]); + + expect(toJSON(result)).to.deepEqual([toJSON(pen)]); + }); +}); diff --git a/acceptance/repository-postgresql/tsconfig.json b/acceptance/repository-postgresql/tsconfig.json index 771d414bef7f..1c7832af7d75 100644 --- a/acceptance/repository-postgresql/tsconfig.json +++ b/acceptance/repository-postgresql/tsconfig.json @@ -15,6 +15,9 @@ }, { "path": "../../packages/repository/tsconfig.json" + }, + { + "path": "../../packages/testlab/tsconfig.json" } ] } diff --git a/packages/repository-tests/src/crud-test-suite.ts b/packages/repository-tests/src/crud-test-suite.ts index c25766341684..8db699931e13 100644 --- a/packages/repository-tests/src/crud-test-suite.ts +++ b/packages/repository-tests/src/crud-test-suite.ts @@ -48,9 +48,16 @@ export function crudRepositoryTestSuite( ); before( withCrudCtx(function setupGlobalDataSource(ctx: CrudTestContext) { + debug('Initializing the shared datasource instance.'); ctx.dataSource = new juggler.DataSource(ctx.dataSourceOptions); }), ); + after( + withCrudCtx(function destroyGlobalDataSource(ctx: CrudTestContext) { + debug('Disconnecting the shared datasource instance.'); + return ctx.dataSource.disconnect(); + }), + ); let testFiles = []; diff --git a/packages/repository-tests/src/crud/transactions.suite.ts b/packages/repository-tests/src/crud/transactions.suite.ts index b1fab9c3a853..88db5a4346b6 100644 --- a/packages/repository-tests/src/crud/transactions.suite.ts +++ b/packages/repository-tests/src/crud/transactions.suite.ts @@ -60,6 +60,8 @@ export function transactionSuite( >; let tx: Transaction | undefined; let ds: juggler.DataSource; + let ds2: juggler.DataSource; + before( withCrudCtx(async function setupRepository(ctx: CrudTestContext) { ds = ctx.dataSource; @@ -85,6 +87,13 @@ export function transactionSuite( } }); + afterEach(async () => { + if (ds2) { + ds2.disconnect(); + (ds2 as unknown) = undefined; + } + }); + it('retrieves model instance once transaction is committed', async () => { tx = await repo.beginTransaction(IsolationLevel.READ_COMMITTED); const created = await repo.create( @@ -149,7 +158,7 @@ export function transactionSuite( const ds2Options = Object.assign({}, dataSourceOptions); ds2Options.name = 'anotherDataSource'; ds2Options.database = ds2Options.database + '_new'; - const ds2 = new juggler.DataSource(ds2Options); + ds2 = new juggler.DataSource(ds2Options); const anotherRepo = new repositoryClass(Product, ds2); await ds2.automigrate(Product.name);