diff --git a/packages/repository-typeorm/.gitignore b/packages/repository-typeorm/.gitignore
new file mode 100644
index 000000000000..90a8d96cc3ff
--- /dev/null
+++ b/packages/repository-typeorm/.gitignore
@@ -0,0 +1,3 @@
+*.tgz
+dist*
+package
diff --git a/packages/repository-typeorm/.npmrc b/packages/repository-typeorm/.npmrc
new file mode 100644
index 000000000000..43c97e719a5a
--- /dev/null
+++ b/packages/repository-typeorm/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/packages/repository-typeorm/COMPARISON.md b/packages/repository-typeorm/COMPARISON.md
new file mode 100644
index 000000000000..3393f19836b2
--- /dev/null
+++ b/packages/repository-typeorm/COMPARISON.md
@@ -0,0 +1,61 @@
+# Feature comparison between LoopBack and TypeORM
+
+## Major features
+
+| Feature | TypeORM | LoopBack |
+| ----------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Databases | MySQL
PostgreSQL
MariaDB
SQLite
MS SQL Server
MongoDB
Oracle
WebSQL | In-memory
MySQL
PostgreSQL
SQLite
MS SQL Server
Oracle
MongoDB
Cloudant
CouchDB
DB2
DB2 Z
DB2 iSeries
DashDB
Informix
Cassandra
Key Value (...)
ElasticSearch
https://github.com/pasindud/awesome-loopback
https://www.npmjs.com/search?q=loopback-connector (189 hits) |
+| Model Definition | TypeScript classes with decorators | JSON
TypeScript classes with decorators |
+| Data access patterns | ActiveRecord (BaseEntity)
EntityManager
Repository | ActiveRecord (3.x Model)
Repository (4.x) |
+| Database Adapters | QueryRunner (monorepo) | Connector (multiple repos/modules) |
+| Query | QueryBuilder | Filter JSON Object
FilterBuilder (4.x) |
+| Scopes | No | Yes |
+| Mutation | QueryBuilder | Where JSON Object |
+| Feedback from mutations | No | Yes |
+| Mixins | No | Yes |
+| Transaction | Yes | Yes |
+| Optimistic Locking | Yes | No (requested by AppConnect team) |
+| Connection pooling | Yes | Yes |
+| Relations | Yes | Yes |
+| Relational Join | Yes | No |
+| Discovery | No | Yes |
+| Migration | Yes | Yes |
+
+## Side by side claims by TypeORM
+
+| Claimed by TypeORM | LoopBack |
+| ---------------------------------------------------------------- | -------------------------------------------------------------------- |
+| supports both DataMapper and ActiveRecord (your choice) | DataMapper is supported by Repository and ActiveRecord by 3.x models |
+| entities and columns | Yes |
+| database-specific column types | Yes |
+| entity manager | Connectors |
+| repositories and custom repositories | Yes |
+| clean object relational model | Yes |
+| associations (relations) | Yes |
+| eager and lazy relations | Yes |
+| uni-directional, bi-directional and self-referenced relations | Yes |
+| supports multiple inheritance patterns | Polymorphic relations |
+| cascades | No |
+| indices | Yes |
+| transactions | Yes |
+| migrations and automatic migrations generation | Yes |
+| connection pooling | Yes |
+| replication | Depending on the driver |
+| using multiple database connections | Yes |
+| working with multiple databases types | Yes |
+| cross-database and cross-schema queries | Yes |
+| elegant-syntax, flexible and powerful QueryBuilder | Poc in 4 |
+| left and inner joins | No |
+| proper pagination for queries using joins | No |
+| query caching | No |
+| streaming raw results | No |
+| logging | Yes - debug |
+| listeners and subscribers (hooks) | Yes |
+| supports closure table pattern | No |
+| schema declaration in models or separate configuration files | Yes |
+| connection configuration in json / xml / yml / env formats | Yes |
+| works in NodeJS / Browser / Ionic / Cordova / Electron platforms | NodeJS only, 3.x is compatible with Browser |
+| TypeScript and JavaScript support | Yes |
+| produced code is performant, flexible, clean and maintainable | ? |
+| follows all possible best practices | ? |
+| CLI | Yes |
diff --git a/packages/repository-typeorm/LICENSE b/packages/repository-typeorm/LICENSE
new file mode 100644
index 000000000000..f78c63f15825
--- /dev/null
+++ b/packages/repository-typeorm/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) IBM Corp. 2017. All Rights Reserved.
+Node module: @loopback/repository
+This project is licensed under the MIT License, full text below.
+
+--------
+
+MIT license
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/packages/repository-typeorm/README.md b/packages/repository-typeorm/README.md
new file mode 100644
index 000000000000..70fd5770f579
--- /dev/null
+++ b/packages/repository-typeorm/README.md
@@ -0,0 +1,16 @@
+# @loopback/repository-typeorm
+
+Repository integration with TypeORM
+
+## Tests
+
+run 'npm test' from the root folder.
+
+## Contributors
+
+See
+[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors).
+
+## License
+
+MIT
diff --git a/packages/repository-typeorm/docs.json b/packages/repository-typeorm/docs.json
new file mode 100644
index 000000000000..726d8b0bb744
--- /dev/null
+++ b/packages/repository-typeorm/docs.json
@@ -0,0 +1,8 @@
+{
+ "content": ["./index.ts", "./src/**/*.ts"],
+ "codeSectionDepth": 4,
+ "assets": {
+ "/": "/docs",
+ "/docs": "/docs"
+ }
+}
diff --git a/packages/repository-typeorm/index.d.ts b/packages/repository-typeorm/index.d.ts
new file mode 100644
index 000000000000..1439c16c90db
--- /dev/null
+++ b/packages/repository-typeorm/index.d.ts
@@ -0,0 +1,6 @@
+// Copyright IBM Corp. 2017. All Rights Reserved.
+// Node module: @loopback/repository
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+export * from './dist/src';
diff --git a/packages/repository-typeorm/index.js b/packages/repository-typeorm/index.js
new file mode 100644
index 000000000000..2c0a0c33fcd2
--- /dev/null
+++ b/packages/repository-typeorm/index.js
@@ -0,0 +1,9 @@
+// Copyright IBM Corp. 2017. All Rights Reserved.
+// Node module: @loopback/repository
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+const nodeMajorVersion = +process.versions.node.split('.')[0];
+module.exports = nodeMajorVersion >= 7 ?
+ require('./dist/src') :
+ require('./dist6/src');
diff --git a/packages/repository-typeorm/index.ts b/packages/repository-typeorm/index.ts
new file mode 100644
index 000000000000..313103704102
--- /dev/null
+++ b/packages/repository-typeorm/index.ts
@@ -0,0 +1,7 @@
+// Copyright IBM Corp. 2013,2017. All Rights Reserved.
+// Node module: @loopback/repository
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+// NOTE(bajtos) This file is used by VSCode/TypeScriptServer at dev time only
+export * from './src';
diff --git a/packages/repository-typeorm/mysql.sh b/packages/repository-typeorm/mysql.sh
new file mode 100755
index 000000000000..84d9ad148847
--- /dev/null
+++ b/packages/repository-typeorm/mysql.sh
@@ -0,0 +1,122 @@
+#!/bin/bash
+
+### Shell script to spin up a docker container for mysql.
+
+## color codes
+RED='\033[1;31m'
+GREEN='\033[1;32m'
+YELLOW='\033[1;33m'
+CYAN='\033[1;36m'
+PLAIN='\033[0m'
+
+## variables
+MYSQL_CONTAINER="mysql_c"
+HOST="localhost"
+USER="root"
+PASSWORD="pass"
+PORT=3306
+DATABASE="testdb"
+if [ "$1" ]; then
+ HOST=$1
+fi
+if [ "$2" ]; then
+ PORT=$2
+fi
+if [ "$3" ]; then
+ USER=$3
+fi
+if [ "$4" ]; then
+ PASSWORD=$4
+fi
+if [ "$5" ]; then
+ DATABASE=$5
+fi
+
+## check if docker exists
+printf "\n${RED}>> Checking for docker${PLAIN} ${GREEN}...${PLAIN}"
+docker -v > /dev/null 2>&1
+DOCKER_EXISTS=$?
+if [ "$DOCKER_EXISTS" -ne 0 ]; then
+ printf "\n\n${CYAN}Status: ${PLAIN}${RED}Docker not found. Terminating setup.${PLAIN}\n\n"
+ exit 1
+fi
+printf "\n${CYAN}Found docker. Moving on with the setup.${PLAIN}\n"
+
+
+## cleaning up previous builds
+printf "\n${RED}>> Finding old builds and cleaning up${PLAIN} ${GREEN}...${PLAIN}"
+docker rm -f $MYSQL_CONTAINER > /dev/null 2>&1
+printf "\n${CYAN}Clean up complete.${PLAIN}\n"
+
+## pull latest mysql image
+printf "\n${RED}>> Pulling latest mysql image${PLAIN} ${GREEN}...${PLAIN}"
+docker pull mysql:latest > /dev/null 2>&1
+printf "\n${CYAN}Image successfully built.${PLAIN}\n"
+
+## run the mysql container
+printf "\n${RED}>> Starting the mysql container${PLAIN} ${GREEN}...${PLAIN}"
+CONTAINER_STATUS=$(docker run --name $MYSQL_CONTAINER -e MYSQL_ROOT_USER=$USER -e MYSQL_ROOT_PASSWORD=$PASSWORD -p $PORT:3306 -d mysql:latest 2>&1)
+if [[ "$CONTAINER_STATUS" == *"Error"* ]]; then
+ printf "\n\n${CYAN}Status: ${PLAIN}${RED}Error starting container. Terminating setup.${PLAIN}\n\n"
+ exit 1
+fi
+docker cp ./test/schema.sql $MYSQL_CONTAINER:/home/ > /dev/null 2>&1
+printf "\n${CYAN}Container is up and running.${PLAIN}\n"
+
+## export the schema to the mysql database
+printf "\n${RED}>> Exporting default schema${PLAIN} ${GREEN}...${PLAIN}\n"
+
+## command to export schema
+docker exec -it $MYSQL_CONTAINER /bin/sh -c "mysql -u$USER -p$PASSWORD < /home/schema.sql" > /dev/null 2>&1
+
+## variables needed to health check export schema
+OUTPUT=$?
+TIMEOUT=120
+TIME_PASSED=0
+WAIT_STRING="."
+
+printf "\n${GREEN}Waiting for mysql to respond with updated schema $WAIT_STRING${PLAIN}"
+while [ "$OUTPUT" -ne 0 ] && [ "$TIMEOUT" -gt 0 ]
+ do
+ docker exec -it $MYSQL_CONTAINER /bin/sh -c "mysql -u$USER -p$PASSWORD < /home/schema.sql" > /dev/null 2>&1
+ OUTPUT=$?
+ sleep 1s
+ TIMEOUT=$((TIMEOUT - 1))
+ TIME_PASSED=$((TIME_PASSED + 1))
+
+ if [ "$TIME_PASSED" -eq 5 ]; then
+ printf "${GREEN}.${PLAIN}"
+ TIME_PASSED=0
+ fi
+ done
+
+if [ "$TIMEOUT" -le 0 ]; then
+ printf "\n\n${CYAN}Status: ${PLAIN}${RED}Failed to export schema. Terminating setup.${PLAIN}\n\n"
+ exit 1
+fi
+printf "\n${CYAN}Successfully exported schema to database.${PLAIN}\n"
+
+## create the database
+printf "\n${RED}>> Creating the database${PLAIN} ${GREEN}...${PLAIN}"
+docker exec -it $MYSQL_CONTAINER /bin/sh -c "mysql -u$USER -p$PASSWORD -e \"DROP DATABASE IF EXISTS $DATABASE\"" > /dev/null 2>&1
+docker exec -it $MYSQL_CONTAINER /bin/sh -c "mysql -u$USER -p$PASSWORD -e \"CREATE DATABASE $DATABASE\"" > /dev/null 2>&1
+DATABASE_CREATED=$?
+if [ "$DATABASE_CREATED" -ne 0 ]; then
+ printf "\n\n${CYAN}Status: ${PLAIN}${RED}Database could not be created. Terminating setup.${PLAIN}\n\n"
+ exit 1
+fi
+printf "\n${CYAN}Successfully created the database.${PLAIN}\n"
+
+## set env variables for running test
+printf "\n${RED}>> Setting env variables to run test${PLAIN} ${GREEN}...${PLAIN}"
+export MYSQL_HOST=$HOST
+export MYSQL_PORT=$PORT
+export MYSQL_USER=$USER
+export MYSQL_PASSWORD=$PASSWORD
+export MYSQL_DATABASE=$DATABASE
+printf "\n${CYAN}Env variables set.${PLAIN}\n"
+
+printf "\n${CYAN}Status: ${PLAIN}${GREEN}Set up completed successfully.${PLAIN}\n"
+printf "\n${CYAN}Instance url: ${YELLOW}mysql://$USER:$PASSWORD@$HOST/$DATABASE${PLAIN}\n"
+printf "\n${CYAN}To run the test suite:${PLAIN} ${YELLOW}npm test${PLAIN}\n\n"
+
diff --git a/packages/repository-typeorm/package.json b/packages/repository-typeorm/package.json
new file mode 100644
index 000000000000..fe15d17f3fb1
--- /dev/null
+++ b/packages/repository-typeorm/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@loopback/repository-typeorm",
+ "version": "4.0.0-alpha.1",
+ "description": "Repository based on TypeORM",
+ "engines": {
+ "node": ">=6"
+ },
+ "main": "index",
+ "scripts": {
+ "acceptance": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/acceptance/**/*.js'",
+ "build": "npm run build:dist && npm run build:dist6",
+ "build:current": "lb-tsc",
+ "build:dist": "lb-tsc es2017",
+ "build:dist6": "lb-tsc es2015",
+ "build:apidocs": "lb-apidocs",
+ "clean": "rm -rf loopback-context*.tgz dist* package",
+ "prepare": "npm run build && npm run build:apidocs",
+ "pretest": "npm run build:current",
+ "test": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/unit/**/*.js' 'DIST/test/acceptance/**/*.js' 'DIST/test/integration/**/*.js'",
+ "integration": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/integration/**/*.js'",
+ "unit": "lb-dist mocha --opts ../../test/mocha.opts 'DIST/test/unit/**/*.js'",
+ "verify": "npm pack && tar xf loopback-juggler*.tgz && tree package && npm run clean"
+ },
+ "author": "IBM",
+ "license": "MIT",
+ "devDependencies": {
+ "@loopback/build": "^4.0.0-alpha.5",
+ "@loopback/testlab": "^4.0.0-alpha.14",
+ "@types/debug": "0.0.30",
+ "mysql": "^2.15.0"
+ },
+ "dependencies": {
+ "@loopback/context": "^4.0.0-alpha.20",
+ "@loopback/core": "^4.0.0-alpha.22",
+ "@loopback/repository": "^4.0.0-alpha.16",
+ "@loopback/rest": "^4.0.0-alpha.9",
+ "debug": "^3.1.0",
+ "typeorm": "^0.1.6"
+ },
+ "files": [
+ "README.md",
+ "index.js",
+ "index.d.ts",
+ "dist/src",
+ "dist6/src",
+ "api-docs",
+ "src"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/strongloop/loopback-next.git"
+ }
+}
diff --git a/packages/repository-typeorm/src/index.ts b/packages/repository-typeorm/src/index.ts
new file mode 100644
index 000000000000..ee8aa43dc49f
--- /dev/null
+++ b/packages/repository-typeorm/src/index.ts
@@ -0,0 +1,6 @@
+// Copyright IBM Corp. 2017. All Rights Reserved.
+// Node module: @loopback/repository-typeorm
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+export * from './repositories';
diff --git a/packages/repository-typeorm/src/repositories/index.ts b/packages/repository-typeorm/src/repositories/index.ts
new file mode 100644
index 000000000000..b0310db0f557
--- /dev/null
+++ b/packages/repository-typeorm/src/repositories/index.ts
@@ -0,0 +1,7 @@
+// Copyright IBM Corp. 2017. All Rights Reserved.
+// Node module: @loopback/repository-typeorm
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+export * from './typeorm-repository';
+export * from './typeorm-datasource';
diff --git a/packages/repository-typeorm/src/repositories/typeorm-datasource.ts b/packages/repository-typeorm/src/repositories/typeorm-datasource.ts
new file mode 100644
index 000000000000..c6a8a5871b2f
--- /dev/null
+++ b/packages/repository-typeorm/src/repositories/typeorm-datasource.ts
@@ -0,0 +1,39 @@
+// Copyright IBM Corp. 2017. All Rights Reserved.
+// Node module: @loopback/repository-typeorm
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+import {
+ createConnection,
+ ConnectionOptions,
+ Connection,
+ ObjectType,
+ Repository,
+ EntityManager,
+} from 'typeorm';
+
+export class TypeORMDataSource {
+ connection: Connection;
+
+ constructor(public settings: ConnectionOptions) {}
+
+ async connect(): Promise {
+ this.connection = await createConnection(this.settings);
+ return this.connection;
+ }
+
+ async disconnect(): Promise {
+ if (!this.connection) return;
+ await this.connection.close();
+ }
+
+ async getEntityManager() {
+ await this.connect();
+ return this.connection.createEntityManager();
+ }
+
+ async getRepository(entityClass: ObjectType): Promise> {
+ await this.connect();
+ return this.connection.getRepository(entityClass);
+ }
+}
diff --git a/packages/repository-typeorm/src/repositories/typeorm-repository.ts b/packages/repository-typeorm/src/repositories/typeorm-repository.ts
new file mode 100644
index 000000000000..3dc1b622a3cc
--- /dev/null
+++ b/packages/repository-typeorm/src/repositories/typeorm-repository.ts
@@ -0,0 +1,291 @@
+// Copyright IBM Corp. 2017. All Rights Reserved.
+// Node module: @loopback/repository-typeorm
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+import {
+ EntityCrudRepository,
+ Entity,
+ DataObject,
+ Options,
+ Filter,
+ Where,
+ AnyObject,
+} from '@loopback/repository';
+import {
+ getRepository,
+ Repository,
+ SelectQueryBuilder,
+ QueryBuilder,
+ UpdateQueryBuilder,
+ DeleteQueryBuilder,
+} from 'typeorm';
+import {DeepPartial} from 'typeorm/common/DeepPartial';
+import {OrderByCondition} from 'typeorm/find-options/OrderByCondition';
+
+import {TypeORMDataSource} from './typeorm-datasource';
+
+import * as debugModule from 'debug';
+const debug = debugModule('loopback:repository:typeorm');
+
+/**
+ * An implementation of EntityCrudRepository using TypeORM
+ */
+export class TypeORMRepository
+ implements EntityCrudRepository {
+ typeOrmRepo: Repository;
+
+ constructor(
+ public dataSource: TypeORMDataSource,
+ public entityClass: typeof Entity & {prototype: T},
+ ) {}
+
+ private async init() {
+ if (this.typeOrmRepo != null) return;
+ this.typeOrmRepo = >await this.dataSource.getRepository(
+ this.entityClass,
+ );
+ }
+
+ async save(entity: DataObject, options?: Options): Promise {
+ await this.init();
+ const result = await this.typeOrmRepo.save(>entity);
+ return result;
+ }
+
+ async update(entity: DataObject, options?: Options): Promise {
+ await this.init();
+ await this.typeOrmRepo.updateById(entity.getId(), >entity);
+ return true;
+ }
+
+ async delete(entity: DataObject, options?: Options): Promise {
+ await this.init();
+ await this.typeOrmRepo.deleteById(entity.getId());
+ return true;
+ }
+
+ async findById(id: ID, filter?: Filter, options?: Options): Promise {
+ await this.init();
+ const result = await this.typeOrmRepo.findOneById(id);
+ if (result == null) {
+ throw new Error('Not found');
+ }
+ return result;
+ }
+
+ async updateById(
+ id: ID,
+ data: DataObject,
+ options?: Options,
+ ): Promise {
+ await this.init();
+ await this.typeOrmRepo.updateById(data.getId(), >data);
+ return true;
+ }
+
+ async replaceById(
+ id: ID,
+ data: DataObject,
+ options?: Options,
+ ): Promise {
+ await this.init();
+ // FIXME [rfeng]: TypeORM doesn't have a method for `replace`
+ await this.typeOrmRepo.updateById(data.getId(), >data);
+ return true;
+ }
+
+ async deleteById(id: ID, options?: Options): Promise {
+ await this.init();
+ await this.typeOrmRepo.deleteById(id);
+ return true;
+ }
+
+ async exists(id: ID, options?: Options): Promise {
+ await this.init();
+ const result = await this.typeOrmRepo.findOneById(id);
+ return result != null;
+ }
+
+ async create(dataObject: DataObject, options?: Options): Promise {
+ await this.init();
+ // Please note typeOrmRepo.create() only instantiates model instances.
+ // It does not persist to the database.
+ const result = await this.typeOrmRepo.save(>dataObject);
+ return result;
+ }
+
+ async createAll(
+ dataObjects: DataObject[],
+ options?: Options,
+ ): Promise {
+ await this.init();
+ const result = await this.typeOrmRepo.save([]>dataObjects);
+ return result;
+ }
+
+ async find(filter?: Filter, options?: Options): Promise {
+ await this.init();
+ const queryBuilder = await this.buildQuery(filter);
+ if (debug.enabled) debug('find: %s', queryBuilder.getSql());
+ const result = queryBuilder.getMany();
+ return result;
+ }
+
+ async updateAll(
+ dataObject: DataObject,
+ where?: Where,
+ options?: Options,
+ ): Promise {
+ await this.init();
+ const queryBuilder = await this.buildUpdate(dataObject, where);
+ if (debug.enabled) debug('updateAll: %s', queryBuilder.getSql());
+ // FIXME [rfeng]: The result is raw data from the DB driver and it varies
+ // between different DBs
+ const result = await queryBuilder.execute();
+ return result;
+ }
+
+ async deleteAll(where?: Where, options?: Options): Promise {
+ await this.init();
+ const queryBuilder = await this.buildDelete(where);
+ if (debug.enabled) debug('deleteAll: %s', queryBuilder.getSql());
+ // FIXME [rfeng]: The result is raw data from the DB driver and it varies
+ // between different DBs
+ const result = await queryBuilder.execute();
+ return result;
+ }
+
+ async count(where?: Where, options?: Options): Promise {
+ await this.init();
+ const result = await this.typeOrmRepo.count(>where);
+ return result;
+ }
+
+ async execute(
+ query: string | AnyObject,
+ // tslint:disable:no-any
+ parameters: AnyObject | any[],
+ options?: Options,
+ ): Promise {
+ await this.init();
+ const result = await this.typeOrmRepo.query(
+ query,
+ parameters,
+ );
+ return result;
+ }
+
+ /**
+ * Convert order clauses to OrderByCondition
+ * @param order An array of orders
+ */
+ buildOrder(order: string[]) {
+ let orderBy: OrderByCondition = {};
+ for (const o of order) {
+ const match = /^([^\s]+)( (ASC|DESC))?$/.exec(o);
+ if (!match) continue;
+ const field = match[1];
+ const dir = (match[3] || 'ASC') as 'ASC' | 'DESC';
+ orderBy[match[1]] = dir;
+ }
+ return orderBy;
+ }
+
+ /**
+ * Build a TypeORM query from LoopBack Filter
+ * @param filter Filter object
+ */
+ async buildQuery(filter?: Filter): Promise> {
+ await this.init();
+ const queryBuilder = this.typeOrmRepo.createQueryBuilder();
+ if (!filter) return queryBuilder;
+ queryBuilder.limit(filter.limit).offset(filter.offset);
+ if (filter.fields) {
+ queryBuilder.select(Object.keys(filter.fields));
+ }
+ if (filter.order) {
+ queryBuilder.orderBy(this.buildOrder(filter.order));
+ }
+ if (filter.where) {
+ queryBuilder.where(this.buildWhere(filter.where));
+ }
+ return queryBuilder;
+ }
+
+ /**
+ * Convert where object into where clause
+ * @param where Where object
+ */
+ buildWhere(where: Where): string {
+ const clauses: string[] = [];
+ if (where.and) {
+ const and = where.and.map(w => `(${this.buildWhere(w)})`).join(' AND ');
+ clauses.push(and);
+ }
+ if (where.or) {
+ const or = where.or.map(w => `(${this.buildWhere(w)})`).join(' OR ');
+ clauses.push(or);
+ }
+ // FIXME [rfeng]: Build parameterized clauses
+ for (const key in where) {
+ let clause;
+ if (key === 'and' || key === 'or') continue;
+ const condition = where[key];
+ if (condition.eq) {
+ clause = `${key} = ${condition.eq}`;
+ } else if (condition.neq) {
+ clause = `${key} != ${condition.neq}`;
+ } else if (condition.lt) {
+ clause = `${key} < ${condition.lt}`;
+ } else if (condition.lte) {
+ clause = `${key} <= ${condition.lte}`;
+ } else if (condition.gt) {
+ clause = `${key} > ${condition.gt}`;
+ } else if (condition.gte) {
+ clause = `${key} >= ${condition.gte}`;
+ } else if (condition.inq) {
+ const vals = condition.inq.join(', ');
+ clause = `${key} IN (${vals})`;
+ } else if (condition.between) {
+ const v1 = condition.between[0];
+ const v2 = condition.between[1];
+ clause = `${key} BETWEEN ${v1} AND ${v2}`;
+ } else {
+ // Shorthand form: {x:1} => X = 1
+ clause = `${key} = ${condition}`;
+ }
+ clauses.push(clause);
+ }
+ return clauses.join(' AND ');
+ }
+
+ /**
+ * Build an `update` statement from LoopBack-style parameters
+ * @param dataObject Data object to be updated
+ * @param where Where object
+ */
+ async buildUpdate(dataObject: DataObject, where?: Where) {
+ await this.init();
+ let queryBuilder = this.typeOrmRepo
+ .createQueryBuilder()
+ .update(this.entityClass)
+ .set(dataObject);
+ if (where) queryBuilder.where(this.buildWhere(where));
+ return queryBuilder;
+ }
+
+ /**
+ * Build a `delete` statement from LoopBack-style parameters
+ * @param where Where object
+ */
+ async buildDelete(where?: Where) {
+ await this.init();
+ let queryBuilder = this.typeOrmRepo
+ .createQueryBuilder()
+ .delete()
+ .from(this.entityClass);
+ if (where) queryBuilder.where(this.buildWhere(where));
+ return queryBuilder;
+ }
+}
diff --git a/packages/repository-typeorm/test/acceptance/typeorm-repository.ts b/packages/repository-typeorm/test/acceptance/typeorm-repository.ts
new file mode 100644
index 000000000000..570d3bef408f
--- /dev/null
+++ b/packages/repository-typeorm/test/acceptance/typeorm-repository.ts
@@ -0,0 +1,81 @@
+// Copyright IBM Corp. 2017. All Rights Reserved.
+// Node module: @loopback/repository-typeorm
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+import {TypeORMDataSource, TypeORMRepository} from '../..';
+import {MysqlConnectionOptions} from 'typeorm/driver/mysql/MysqlConnectionOptions';
+
+import {Entity as Base} from '@loopback/repository';
+import {Entity, Column, PrimaryGeneratedColumn, Repository} from 'typeorm';
+
+describe('TypeORM Repository', () => {
+ @Entity('NOTE')
+ class Note extends Base {
+ @PrimaryGeneratedColumn() id: number;
+
+ @Column({
+ length: 100,
+ })
+ title: string;
+
+ @Column('text') content: string;
+ }
+
+ const options: MysqlConnectionOptions = {
+ type: 'mysql',
+ host: 'localhost',
+ port: 3306,
+ username: 'root',
+ password: 'pass',
+ database: 'TESTDB',
+ entities: [Note],
+ };
+
+ let repository: TypeORMRepository;
+
+ before(() => {
+ const ds = new TypeORMDataSource(options);
+ repository = new TypeORMRepository(ds, Note);
+ });
+
+ it('creates new instances', async () => {
+ let result = await repository.create({
+ title: 'Note1',
+ content: 'This is note #1',
+ });
+ console.log(result);
+ result = await repository.create({
+ title: 'Note2',
+ content: 'This is note #2',
+ });
+ console.log(result);
+ });
+
+ it('finds matching instances', async () => {
+ const result = await repository.find();
+ console.log(result);
+ });
+
+ it('finds matching instances with filter', async () => {
+ const result = await repository.find({
+ limit: 2,
+ order: ['title DESC'],
+ where: {id: {lt: 5}},
+ });
+ console.log(result);
+ });
+
+ it('updates matching instances', async () => {
+ const result = await repository.updateAll(
+ {content: 'This is note #2 - edited'},
+ {id: 2},
+ );
+ console.log(result);
+ });
+
+ it('deletes all instances', async () => {
+ const result = await repository.deleteAll({});
+ console.log(result);
+ });
+});
diff --git a/packages/repository-typeorm/test/schema.sql b/packages/repository-typeorm/test/schema.sql
new file mode 100644
index 000000000000..f85fd5ae3233
--- /dev/null
+++ b/packages/repository-typeorm/test/schema.sql
@@ -0,0 +1,25 @@
+--
+-- Current Database: `TESTDB`
+--
+
+/*!40000 DROP DATABASE IF EXISTS `TESTDB`*/;
+
+CREATE DATABASE /*!32312 IF NOT EXISTS*/ `TESTDB` /*!40100 DEFAULT CHARACTER SET utf8 */;
+
+USE `TESTDB`;
+
+--
+-- Table structure for table `NOTE`
+--
+
+DROP TABLE IF EXISTS `NOTE`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `NOTE` (
+ `ID` int(11) NOT NULL AUTO_INCREMENT,
+ `TITLE` varchar(64) DEFAULT NULL,
+ `CONTENT` text DEFAULT NULL,
+ PRIMARY KEY (`ID`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+
diff --git a/packages/repository-typeorm/tsconfig.build.json b/packages/repository-typeorm/tsconfig.build.json
new file mode 100644
index 000000000000..855e02848b35
--- /dev/null
+++ b/packages/repository-typeorm/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "http://json.schemastore.org/tsconfig",
+ "extends": "../build/config/tsconfig.common.json",
+ "compilerOptions": {
+ "rootDir": "."
+ },
+ "include": ["src", "test"]
+}
diff --git a/packages/repository/src/connector.ts b/packages/repository/src/connector.ts
index 3eb7bbb9dcb8..c391ab431689 100644
--- a/packages/repository/src/connector.ts
+++ b/packages/repository/src/connector.ts
@@ -4,6 +4,7 @@
// License text available at https://opensource.org/licenses/MIT
import {Model} from './model';
+import {AnyObject, Options} from '../index';
/**
* Common properties/operations for connectors
@@ -15,4 +16,10 @@ export interface Connector {
connect(): Promise; // Connect to the underlying system
disconnect(): Promise; // Disconnect from the underlying system
ping(): Promise; // Ping the underlying system
+ execute?(
+ query: string | AnyObject,
+ // tslint:disable:no-any
+ parameters: AnyObject | any[],
+ options?: Options,
+ ): Promise;
}
diff --git a/packages/repository/src/legacy-juggler-bridge.ts b/packages/repository/src/legacy-juggler-bridge.ts
index 09963a40eb05..38bf7f516e43 100644
--- a/packages/repository/src/legacy-juggler-bridge.ts
+++ b/packages/repository/src/legacy-juggler-bridge.ts
@@ -14,6 +14,7 @@ import {EntityCrudRepository} from './repository';
export * from './loopback-datasource-juggler';
import {juggler} from './loopback-datasource-juggler';
+import {AnyObject} from '../index';
type DataSourceType = juggler.DataSource;
export {DataSourceType};
@@ -195,6 +196,15 @@ export class DefaultCrudRepository
return ensurePromise(this.modelClass.exists(id, options));
}
+ async execute(
+ query: string | AnyObject,
+ // tslint:disable:no-any
+ parameters: AnyObject | any[],
+ options?: Options,
+ ): Promise {
+ throw new Error('Not implemented');
+ }
+
protected toEntity(model: DataObject): T {
return new this.entityClass(model.toObject()) as T;
}
diff --git a/packages/repository/src/loopback-datasource-juggler.ts b/packages/repository/src/loopback-datasource-juggler.ts
index 7d17aa8cf3a5..f14c0a7ecfc2 100644
--- a/packages/repository/src/loopback-datasource-juggler.ts
+++ b/packages/repository/src/loopback-datasource-juggler.ts
@@ -101,6 +101,7 @@ export declare namespace juggler {
* Base model class
*/
export class ModelBase {
+ static dataSource?: DataSource;
static modelName: string;
static definition: ModelDefinition;
static attachTo(ds: DataSource): void;
@@ -202,7 +203,7 @@ export declare namespace juggler {
eq?: any;
neq?: any;
gt?: any;
- get?: any;
+ gte?: any;
lt?: any;
lte?: any;
inq?: any[];
@@ -221,18 +222,6 @@ export declare namespace juggler {
[property: string]: Condition | any; // Other criteria
}
- /**
- * Order by direction
- */
- export type Direction = 'ASC' | 'DESC';
-
- /**
- * Order by
- */
- export interface Order {
- [property: string]: Direction;
- }
-
/**
* Selection of fields
*/
@@ -245,7 +234,7 @@ export declare namespace juggler {
*/
export interface Inclusion {
relation: string;
- scope: Filter;
+ scope?: Filter;
}
/**
@@ -254,7 +243,7 @@ export declare namespace juggler {
export interface Filter {
where?: Where;
fields?: Fields;
- order?: Order[];
+ order?: string[];
limit?: number;
skip?: number;
offset?: number;
diff --git a/packages/repository/src/query.ts b/packages/repository/src/query.ts
index a6a1fa5ca460..bd216d68b9e3 100644
--- a/packages/repository/src/query.ts
+++ b/packages/repository/src/query.ts
@@ -1,3 +1,6 @@
+import {relation, AnyObject} from '../index';
+import * as assert from 'assert';
+
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: @loopback/repository
// This file is licensed under the MIT License.
@@ -29,7 +32,7 @@ export interface Condition {
eq?: any;
neq?: any;
gt?: any;
- get?: any;
+ gte?: any;
lt?: any;
lte?: any;
inq?: any[];
@@ -56,7 +59,7 @@ export interface Where {
/**
* Order by direction
*/
-export type Direction = 'ASC' | 'DESC';
+export type Direction = 'ASC' | 'DESC' | 1 | -1;
/**
* Order by
@@ -88,7 +91,7 @@ export interface Fields {
*/
export interface Inclusion {
relation: string;
- scope: Filter;
+ scope?: Filter;
}
/**
@@ -106,7 +109,7 @@ export interface Filter {
/**
* Sorting order for matched entities
*/
- order?: Order[];
+ order?: string[];
/**
* Maximum number of entities
*/
@@ -124,3 +127,316 @@ export interface Filter {
*/
include?: Inclusion[];
}
+
+/**
+ * A builder for Where object
+ */
+export class WhereBuilder {
+ where: Where;
+
+ constructor(w?: Where) {
+ this.where = w || {};
+ }
+
+ private add(w: Where): this {
+ for (const k of Object.keys(w)) {
+ if (k in this.where) {
+ // Found conflicting keys, create an `and` operator to join the existing
+ // conditions with the new one
+ this.where = {and: [this.where, w]};
+ return this;
+ }
+ }
+ // Merge the where items
+ this.where = Object.assign(this.where, w);
+ return this;
+ }
+
+ /**
+ * Add an `and` clause.
+ * @param w One or more where objects
+ */
+ and(...w: (Where | Where[])[]): this {
+ let clauses: Where[] = [];
+ w.forEach(where => {
+ clauses = clauses.concat(Array.isArray(where) ? where : [where]);
+ });
+ return this.add({and: clauses});
+ }
+
+ /**
+ * Add an `or` clause.
+ * @param w One or more where objects
+ */
+ or(...w: (Where | Where[])[]): this {
+ let clauses: Where[] = [];
+ w.forEach(where => {
+ clauses = clauses.concat(Array.isArray(where) ? where : [where]);
+ });
+ return this.add({or: clauses});
+ }
+
+ /**
+ * Add an `=` condition
+ * @param key Property name
+ * @param val Property value
+ */
+ eq(key: string, val: any): this {
+ return this.add({[key]: val});
+ }
+
+ /**
+ * Add a `!=` condition
+ * @param key Property name
+ * @param val Property value
+ */
+ neq(key: string, val: any): this {
+ return this.add({[key]: {neq: val}});
+ }
+
+ /**
+ * Add a `>` condition
+ * @param key Property name
+ * @param val Property value
+ */
+ gt(key: string, val: any): this {
+ return this.add({[key]: {gt: val}});
+ }
+
+ /**
+ * Add a `>=` condition
+ * @param key Property name
+ * @param val Property value
+ */
+ gte(key: string, val: any): this {
+ return this.add({[key]: {gte: val}});
+ }
+
+ /**
+ * Add a `<` condition
+ * @param key Property name
+ * @param val Property value
+ */
+ lt(key: string, val: any): this {
+ return this.add({[key]: {lt: val}});
+ }
+
+ /**
+ * Add a `<=` condition
+ * @param key Property name
+ * @param val Property value
+ */
+ lte(key: string, val: any): this {
+ return this.add({[key]: {lte: val}});
+ }
+
+ /**
+ * Add a `inq` condition
+ * @param key Property name
+ * @param val An array of property values
+ */
+ inq(key: string, val: any[]): this {
+ return this.add({[key]: {inq: val}});
+ }
+
+ /**
+ * Add a `between` condition
+ * @param key Property name
+ * @param val1 Property value lower bound
+ * @param val2 Property value upper bound
+ */
+ between(key: string, val1: any, val2: any): this {
+ return this.add({[key]: {between: [val1, val2]}});
+ }
+
+ /**
+ * Add a `exists` condition
+ * @param key Property name
+ * @param val Exists or not
+ */
+ exists(key: string, val?: boolean): this {
+ return this.add({[key]: {exists: !!val || val == null}});
+ }
+
+ /**
+ * Get the where object
+ */
+ build() {
+ return this.where;
+ }
+}
+
+/**
+ * A builder for Filter
+ */
+export class FilterBuilder {
+ filter: Filter;
+
+ constructor(f?: Filter) {
+ this.filter = f || {};
+ }
+
+ /**
+ * Set `limit`
+ * @param limit Maximum number of records to be returned
+ */
+ limit(limit: number): this {
+ assert(limit >= 1, `Limit ${limit} must a positive number`);
+ this.filter.limit = limit;
+ return this;
+ }
+
+ /**
+ * Set `offset`
+ * @param offset Offset of the number of records to be returned
+ */
+ offset(offset: number): this {
+ this.filter.offset = offset;
+ return this;
+ }
+
+ /**
+ * Alias to `offset`
+ * @param skip
+ */
+ skip(skip: number): this {
+ return this.offset(skip);
+ }
+
+ /**
+ * Describe what fields to be included/excluded
+ * @param f A field name to be included, an array of field names to be
+ * included, or an Fields object for the inclusion/exclusion
+ */
+ fields(...f: (Fields | string[] | string)[]): this {
+ if (!this.filter.fields) {
+ this.filter.fields = {};
+ }
+ f.forEach(field => {
+ if (Array.isArray(field)) {
+ field.forEach(i => (this.filter.fields![i] = true));
+ } else if (typeof field === 'string') {
+ this.filter.fields![field] = true;
+ } else {
+ Object.assign(this.filter.fields, field);
+ }
+ });
+ return this;
+ }
+
+ private validateOrder(order: string) {
+ assert(order.match(/^[^\s]+( (ASC|DESC))?$/), 'Invalid order: ' + order);
+ }
+
+ /**
+ * Describe the sorting order
+ * @param f A field name with optional direction, an array of field names,
+ * or an Order object for the field/direction pairs
+ */
+ order(...o: (string | string[] | Order)[]): this {
+ if (!this.filter.order) {
+ this.filter.order = [];
+ }
+ o.forEach(order => {
+ if (typeof order === 'string') {
+ this.validateOrder(order);
+ this.filter.order!.push(order);
+ return this;
+ }
+ if (Array.isArray(order)) {
+ order.forEach(this.validateOrder);
+ this.filter.order = this.filter.order!.concat(order);
+ return this;
+ }
+ for (const i in order) {
+ let dir: string;
+ if (order[i] === 1) {
+ dir = 'ASC';
+ } else if (order[i] === -1) {
+ dir = 'DESC';
+ } else {
+ dir = order[i];
+ }
+ this.filter.order!.push(`${i} ${dir}`);
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Declare `include`
+ * @param i A relation name, an array of relation names, or an `Inclusion`
+ * object for the relation/scope definitions
+ */
+ include(...i: (string | string[] | Inclusion)[]): this {
+ if (!this.filter.include) {
+ this.filter.include = [];
+ }
+ i.forEach(include => {
+ if (typeof include === 'string') {
+ this.filter.include!.push({relation: include});
+ } else if (Array.isArray(include)) {
+ include.forEach(inc => this.filter.include!.push({relation: inc}));
+ } else {
+ this.filter.include!.push(include);
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Declare a where clause
+ * @param w Where object
+ */
+ where(w: Where): this {
+ this.filter.where = w;
+ return this;
+ }
+
+ /**
+ * Return the filter object
+ */
+ build() {
+ return this.filter;
+ }
+}
+
+/**
+ * Get nested properties by path
+ * @param value Value of an object
+ * @param path Path to the property
+ */
+function getDeepProperty(value: AnyObject, path: string): any {
+ const props = path.split('.');
+ for (const p of props) {
+ value = value[p];
+ if (value === undefined || value === null) {
+ return value;
+ }
+ }
+ return value;
+}
+
+export function filterTemplate(strings: TemplateStringsArray, ...keys: any[]) {
+ return function filter(ctx: AnyObject) {
+ const tokens = [strings[0]];
+ keys.forEach((key, i) => {
+ if (
+ typeof key === 'object' ||
+ typeof key === 'boolean' ||
+ typeof key === 'number'
+ ) {
+ tokens.push(JSON.stringify(key), strings[i + 1]);
+ return;
+ }
+ const value = getDeepProperty(ctx, key);
+ tokens.push(JSON.stringify(value), strings[i + 1]);
+ });
+ const result = tokens.join('');
+ try {
+ return JSON.parse(result);
+ } catch (e) {
+ throw new Error('Invalid JSON: ' + result);
+ }
+ };
+}
diff --git a/packages/repository/src/repository.ts b/packages/repository/src/repository.ts
index 916910b34de7..48001e3386fc 100644
--- a/packages/repository/src/repository.ts
+++ b/packages/repository/src/repository.ts
@@ -4,14 +4,28 @@
// License text available at https://opensource.org/licenses/MIT
import {Entity, ValueObject, Model} from './model';
-import {Class, DataObject, Options} from './common-types';
+import {Class, DataObject, Options, AnyObject} from './common-types';
import {DataSource} from './datasource';
import {CrudConnector} from './crud-connector';
import {Filter, Where} from './query';
// tslint:disable:no-unused-variable
-export interface Repository {}
+export interface Repository {
+ /**
+ * Execute a query with the given parameter object or an array of parameters
+ * @param query The query string or command
+ * @param parameters The object with name/value pairs or an array of parameter
+ * values
+ * @param options Options
+ */
+ execute(
+ query: string | AnyObject,
+ // tslint:disable:no-any
+ parameters: AnyObject | any[],
+ options?: Options,
+ ): Promise;
+}
/**
* Basic CRUD operations for ValueObject and Entity. No ID is required.
@@ -305,4 +319,16 @@ export class CrudRepositoryImpl
return this.count(where, options).then(result => result > 0);
}
}
+
+ execute(
+ query: string | AnyObject,
+ // tslint:disable:no-any
+ parameters: AnyObject | any[],
+ options?: Options,
+ ): Promise {
+ if (typeof this.connector.execute !== 'function') {
+ throw new Error('Not implemented');
+ }
+ return this.connector.execute(query, parameters, options);
+ }
}
diff --git a/packages/repository/test/unit/query/query-builder.ts b/packages/repository/test/unit/query/query-builder.ts
new file mode 100644
index 000000000000..3ac825fbca91
--- /dev/null
+++ b/packages/repository/test/unit/query/query-builder.ts
@@ -0,0 +1,275 @@
+// Copyright IBM Corp. 2013,2017. All Rights Reserved.
+// Node module: @loopback/repository
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+import {expect} from '@loopback/testlab';
+import {
+ FilterBuilder,
+ Filter,
+ WhereBuilder,
+ Where,
+ filterTemplate,
+} from '../../../';
+
+describe('WhereBuilder', () => {
+ it('builds where object', () => {
+ const whereBuilder = new WhereBuilder();
+ const where = whereBuilder
+ .eq('a', 1)
+ .gt('b', 2)
+ .lt('c', 2)
+ .eq('x', 'x')
+ .build();
+ expect(where).to.eql({a: 1, b: {gt: 2}, c: {lt: 2}, x: 'x'});
+ });
+
+ it('builds where object with multiple clauses using the same key', () => {
+ const whereBuilder = new WhereBuilder();
+ const where = whereBuilder
+ .gt('a', 2)
+ .lt('a', 4)
+ .build();
+ expect(where).to.eql({and: [{a: {gt: 2}}, {a: {lt: 4}}]});
+ });
+
+ it('builds where object with inq', () => {
+ const whereBuilder = new WhereBuilder();
+ const where = whereBuilder
+ .inq('x', [1, 2, 3])
+ .inq('y', ['a', 'b'])
+ .build();
+ expect(where).to.eql({x: {inq: [1, 2, 3]}, y: {inq: ['a', 'b']}});
+ });
+
+ it('builds where object with between', () => {
+ const whereBuilder = new WhereBuilder();
+ const where = whereBuilder
+ .between('x', 1, 2)
+ .between('y', 'a', 'b')
+ .build();
+ expect(where).to.eql({x: {between: [1, 2]}, y: {between: ['a', 'b']}});
+ });
+
+ it('builds where object with or', () => {
+ const whereBuilder = new WhereBuilder();
+ const where = whereBuilder
+ .eq('a', 1)
+ .gt('b', 2)
+ .lt('c', 2)
+ .or({x: 'x'}, {y: {gt: 1}})
+ .build();
+ expect(where).to.eql({
+ a: 1,
+ b: {gt: 2},
+ c: {lt: 2},
+ or: [{x: 'x'}, {y: {gt: 1}}],
+ });
+ });
+
+ it('builds where object with and', () => {
+ const whereBuilder = new WhereBuilder();
+ const where = whereBuilder
+ .eq('a', 1)
+ .gt('b', 2)
+ .lt('c', 2)
+ .and({x: 'x'}, {y: {gt: 1}})
+ .build();
+ expect(where).to.eql({
+ a: 1,
+ b: {gt: 2},
+ c: {lt: 2},
+ and: [{x: 'x'}, {y: {gt: 1}}],
+ });
+ });
+
+ it('builds where object with existing and', () => {
+ const whereBuilder = new WhereBuilder();
+ const where = whereBuilder
+ .eq('a', 1)
+ .and({x: 'x'}, {y: {gt: 1}})
+ .and({b: 'b'}, {c: {lt: 1}})
+ .build();
+ expect(where).to.eql({
+ and: [
+ {
+ a: 1,
+ and: [{x: 'x'}, {y: {gt: 1}}],
+ },
+ {
+ and: [{b: 'b'}, {c: {lt: 1}}],
+ },
+ ],
+ });
+ });
+
+ it('builds where object from an existing one', () => {
+ const whereBuilder = new WhereBuilder({y: 'y'});
+ const where = whereBuilder
+ .eq('a', 1)
+ .gt('b', 2)
+ .lt('c', 2)
+ .eq('x', 'x')
+ .build();
+ expect(where).to.eql({y: 'y', a: 1, b: {gt: 2}, c: {lt: 2}, x: 'x'});
+ });
+});
+
+describe('FilterBuilder', () => {
+ it('builds a filter object with field names', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.fields('a', 'b', 'c');
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ fields: {
+ a: true,
+ b: true,
+ c: true,
+ },
+ });
+ });
+
+ it('builds a filter object with field object', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.fields({a: true, b: false});
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ fields: {
+ a: true,
+ b: false,
+ },
+ });
+ });
+
+ it('builds a filter object with mixed field names and objects', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.fields({a: true, b: false}, 'c', ['d', 'e']);
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ fields: {
+ a: true,
+ b: false,
+ c: true,
+ d: true,
+ e: true,
+ },
+ });
+ });
+
+ it('builds a filter object with limit/offset', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.limit(10).offset(5);
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ limit: 10,
+ offset: 5,
+ });
+ });
+
+ it('builds a filter object with limit/skip', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.limit(10).skip(5);
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ limit: 10,
+ offset: 5,
+ });
+ });
+
+ it('validates limit', () => {
+ expect(() => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.limit(-10).offset(5);
+ }).to.throw(/Limit \-10 must a positive number/);
+ });
+
+ it('builds a filter object with order names', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.order('a', 'b', 'c');
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ order: ['a', 'b', 'c'],
+ });
+ });
+
+ it('builds a filter object with order object', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.order({a: 1, b: -1});
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ order: ['a ASC', 'b DESC'],
+ });
+ });
+
+ it('builds a filter object with mixed field names and objects', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.order({a: 'ASC', b: 'DESC'}, 'c DESC', ['d', 'e']);
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ order: ['a ASC', 'b DESC', 'c DESC', 'd', 'e'],
+ });
+ });
+
+ it('validates order', () => {
+ expect(() => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.order('a x');
+ }).to.throw(/Invalid order/);
+ });
+
+ it('builds a filter object with where', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.where({x: 1, and: [{a: {gt: 2}}, {b: 2}]});
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ where: {x: 1, and: [{a: {gt: 2}}, {b: 2}]},
+ });
+ });
+
+ it('builds a filter object with included relation names', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.include('orders', 'friends');
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ include: [{relation: 'orders'}, {relation: 'friends'}],
+ });
+ });
+
+ it('builds a filter object with included an array of relation names', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.include(['orders', 'friends']);
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ include: [{relation: 'orders'}, {relation: 'friends'}],
+ });
+ });
+
+ it('builds a filter object with inclusion objects', () => {
+ const filterBuilder = new FilterBuilder();
+ filterBuilder.include(
+ {relation: 'orders'},
+ {relation: 'friends', scope: {where: {name: 'ray'}}},
+ );
+ const filter = filterBuilder.build();
+ expect(filter).to.eql({
+ include: [
+ {relation: 'orders'},
+ {relation: 'friends', scope: {where: {name: 'ray'}}},
+ ],
+ });
+ });
+});
+
+describe('FilterTemplate', () => {
+ it('builds filter object', () => {
+ const filter = filterTemplate`{"limit": ${'limit'},
+ "where": {${'key'}: ${'value'}}}`;
+ const result = filter({limit: 10, key: 'name', value: 'John'});
+ expect(result).to.eql({
+ limit: 10,
+ where: {
+ name: 'John',
+ },
+ });
+ });
+});
diff --git a/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts b/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts
index 74ae34fa6926..35dc20df4b37 100644
--- a/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts
+++ b/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts
@@ -9,6 +9,9 @@ import {
juggler,
DataSourceConstructor,
Class,
+ Options,
+ Repository,
+ AnyObject,
} from '../../../';
import {Application, Component} from '@loopback/core';
@@ -69,7 +72,7 @@ describe('RepositoryMixin', () => {
class AppWithRepoMixin extends RepositoryMixin(Application) {}
- class NoteRepo {
+ class NoteRepo implements Repository {
model: any;
constructor() {
@@ -84,6 +87,15 @@ describe('RepositoryMixin', () => {
{},
);
}
+
+ execute(
+ query: string | AnyObject,
+ // tslint:disable:no-any
+ parameters: AnyObject | any[],
+ options?: Options,
+ ): Promise {
+ throw Error('Not implemented');
+ }
}
class TestComponent {