e22m4u 2 лет назад
Сommit
9e8d810694

+ 9 - 0
.c8rc

@@ -0,0 +1,9 @@
+{
+  "all": true,
+  "include": [
+    "src/**/*.js"
+  ],
+  "exclude": [
+    "src/**/*.spec.js"
+  ]
+}

+ 5 - 0
.commitlintrc

@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "@commitlint/config-conventional"
+  ]
+}

+ 13 - 0
.editorconfig

@@ -0,0 +1,13 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+max_line_length = 80

+ 20 - 0
.eslintrc.cjs

@@ -0,0 +1,20 @@
+module.exports = {
+  env: {
+    es2021: true,
+    node: true
+  },
+  parserOptions: {
+    sourceType: 'module',
+    ecmaVersion: 13,
+  },
+  plugins: [
+    'mocha',
+    'chai-expect',
+  ],
+  extends: [
+    'eslint:recommended',
+    'prettier',
+    'plugin:mocha/recommended',
+    'plugin:chai-expect/recommended',
+  ],
+}

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+# OS
+.DS_Store
+
+# IDE
+.idea
+
+# Npm
+node_modules
+npm-debug.log
+package-lock.json
+
+# Yarn
+.yarn/
+.yarn*
+
+# c8
+coverage

+ 4 - 0
.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no -- commitlint --edit "${1}"

+ 9 - 0
.husky/pre-commit

@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm run lint:fix
+npm run format
+
+npm run test
+
+git add -A

+ 7 - 0
.mocharc.cjs

@@ -0,0 +1,7 @@
+const path = require('path');
+
+module.exports = {
+  extension: ['js'],
+  spec: 'src/**/*.spec.js',
+  require: [path.join(__dirname, 'mocha.setup.js')],
+}

+ 7 - 0
.prettierrc

@@ -0,0 +1,7 @@
+{
+  "bracketSpacing": false,
+  "singleQuote": true,
+  "printWidth": 80,
+  "trailingComma": "all",
+  "arrowParens": "avoid"
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 e22m4u@gmail.com
+
+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.

+ 81 - 0
README.md

@@ -0,0 +1,81 @@
+## @e22m4u/repository-mongodb-adapter
+
+MongoDB адаптер для [@e22m4u/repository](https://www.npmjs.com/package/@e22m4u/repository)  
+
+## Установка
+
+
+```bash
+npm install @e22m4u/repository-mongodb-adapter
+```
+
+*требует пакет [repository](https://www.npmjs.com/package/@e22m4u/repository)*
+
+## Параметры
+
+Все указанные параметры опциональны:
+
+| название | значение по умолчанию |
+|----------|-----------------------|
+| protocol | `'mongodb'`           |
+| host     | `'127.0.0.1'`         |
+| port     | `27017`               |
+| database | `'database'`          |
+| username | `undefined`           |
+| password | `undefined`           |
+
+Пример:
+
+```js
+import {Schema} from '@e22m4u/repository';
+
+const schema = new Schema();
+
+// объявление источника
+schema.defineDatasource({
+  name: 'myMongo', // название источника
+  adapter: 'mongodb', // имя адаптера
+  // параметры
+  host: '192.128.0.2',
+  port: 27017,
+})
+
+// объявление модели
+schema.defineModel({
+  name: 'user', // название модели
+  datasource: 'myMongo', // используемый источник
+  properties: { // поля модели
+    name: 'string',
+    surname: 'string',
+  },
+});
+
+// получаем репозиторий по названию модели и создаем запись
+const userRep = schema.getRepository('user');
+const user = await userRep.create({name: 'John', surname: 'Doe'});
+
+console.log(user);
+// {
+//   id: '64f3454e5e0893c13f9bf47e',
+//   name: 'John',
+//   surname: 'Doe',
+// }
+```
+
+## Тесты
+
+Запуск контейнера `mongodb_c` скриптом `setup.sh`
+
+```bash
+./setup.sh
+```
+
+Выполнение тестов
+
+```bash
+npm run test
+```
+
+## Лицензия
+
+MIT

+ 13 - 0
mocha.setup.js

@@ -0,0 +1,13 @@
+import url from 'url';
+import chai from 'chai';
+import dotenv from 'dotenv';
+import chaiSpies from 'chai-spies';
+import chaiAsPromised from 'chai-as-promised';
+
+process.env['NODE_ENV'] = 'test';
+const dirname = url.fileURLToPath(new URL('.', import.meta.url));
+const envFile = `${dirname}/${process.env['NODE_ENV']}.env`;
+dotenv.config({path: envFile});
+
+chai.use(chaiSpies);
+chai.use(chaiAsPromised);

+ 57 - 0
package.json

@@ -0,0 +1,57 @@
+{
+  "name": "@e22m4u/repository-mongodb-adapter",
+  "version": "0.0.3",
+  "description": "MongoDB адаптер для @e22m4u/repository",
+  "type": "module",
+  "main": "src/index.js",
+  "engines": {
+    "node": ">=14"
+  },
+  "scripts": {
+    "lint": "eslint .",
+    "lint:fix": "eslint . --fix",
+    "format": "prettier --write \"./src/**/*.js\"",
+    "test": "eslint . && c8 --reporter=text-summary mocha",
+    "test:coverage": "eslint . && c8 --reporter=text mocha",
+    "prepare": "npx husky install"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/e22m4u/repository-mongodb-adapter.git"
+  },
+  "keywords": [
+    "MongoDB",
+    "Repository",
+    "ORM",
+    "ODM",
+    "Database",
+    "Datasource",
+    "Relation",
+    "Inclusion"
+  ],
+  "author": "e22m4u <e22m4u@gmail.com>",
+  "license": "MIT",
+  "homepage": "https://github.com/e22m4u/repository-mongodb-adapter",
+  "dependencies": {
+    "mongodb": "^5.8.1"
+  },
+  "peerDependencies": {
+    "@e22m4u/repository": "^0.0.12"
+  },
+  "devDependencies": {
+    "@commitlint/cli": "^17.7.1",
+    "@commitlint/config-conventional": "^17.7.0",
+    "c8": "^8.0.1",
+    "chai": "^4.3.7",
+    "chai-as-promised": "^7.1.1",
+    "chai-spies": "^1.0.0",
+    "dotenv": "^16.3.1",
+    "eslint": "^8.47.0",
+    "eslint-config-prettier": "^9.0.0",
+    "eslint-plugin-chai-expect": "^3.0.0",
+    "eslint-plugin-mocha": "^10.1.0",
+    "husky": "^8.0.3",
+    "mocha": "^10.2.0",
+    "prettier": "^3.0.1"
+  }
+}

+ 39 - 0
setup.sh

@@ -0,0 +1,39 @@
+#!/bin/bash
+
+ENV_FILE="test.env"
+SCRIPT_DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)
+
+# Setting env variables
+printf "\nSetting env variables..."
+if [ ! -f .env ]
+then
+  # shellcheck disable=SC2046
+  export $(xargs -a "$SCRIPT_DIR/$ENV_FILE")
+fi
+
+# Checking for docker
+printf "\nChecking for docker...";
+docker -v > /dev/null 2>&1
+DOCKER_EXISTS=$?
+if [ "$DOCKER_EXISTS" -ne 0 ]; then
+    printf "\nDocker not found. Terminating setup."
+    exit 1
+fi
+
+# Pulling latest mongodb image
+printf "\nPulling latest mongodb image..."
+docker pull mongo:latest > /dev/null 2>&1
+
+# Starting the mongodb container
+printf "\nStarting the mongodb container..."
+CONTAINER_EXISTS=$(docker ps -a -q -f name="$MONGODB_CONTAINER")
+if [ "$CONTAINER_EXISTS" ]; then
+    docker rm -f "$MONGODB_CONTAINER" > /dev/null
+fi
+docker run --name "$MONGODB_CONTAINER" -p "$MONGODB_PORT":27017 -d mongo:latest > /dev/null
+
+# Mongodb container has started
+printf "\n\nStatus: Mongodb container has started."
+# shellcheck disable=SC2059
+printf "\nInstance url: mongodb://$MONGODB_HOST:$MONGODB_PORT/$MONGODB_DATABASE"
+printf "\nTo run the test suite: npm test\n\n"

+ 1 - 0
src/index.js

@@ -0,0 +1 @@
+export * from './mongodb-adapter.js';

+ 822 - 0
src/mongodb-adapter.js

@@ -0,0 +1,822 @@
+/* eslint no-unused-vars: 0 */
+import {ObjectId} from 'mongodb';
+import {MongoClient} from 'mongodb';
+import {isObjectId} from './utils/index.js';
+import {Adapter} from '@e22m4u/repository';
+import {DataType} from '@e22m4u/repository';
+import {capitalize} from '@e22m4u/repository';
+import {isIsoDate} from './utils/is-iso-date.js';
+import {createMongodbUrl} from './utils/index.js';
+import {stringToRegexp} from '@e22m4u/repository';
+import {transformValuesDeep} from './utils/index.js';
+import {selectObjectKeys} from '@e22m4u/repository';
+import {ModelDefinitionUtils} from '@e22m4u/repository';
+import {InvalidArgumentError} from '@e22m4u/repository';
+import {InvalidOperatorValueError} from '@e22m4u/repository';
+
+/**
+ * Mongodb option names.
+ * 5.8.1
+ *
+ * @type {string[]}
+ */
+const MONGODB_OPTION_NAMES = [
+  'appname',
+  'authMechanism',
+  'authMechanismProperties',
+  'authSource',
+  'compressors',
+  'connectTimeoutMS',
+  'directConnection',
+  'heartbeatFrequencyMS',
+  'journal',
+  'loadBalanced',
+  'localThresholdMS',
+  'maxIdleTimeMS',
+  'maxPoolSize',
+  'maxConnecting',
+  'maxStalenessSeconds',
+  'minPoolSize',
+  'proxyHost',
+  'proxyPort',
+  'proxyUsername',
+  'proxyPassword',
+  'readConcernLevel',
+  'readPreference',
+  'readPreferenceTags',
+  'replicaSet',
+  'retryReads',
+  'retryWrites',
+  'serverSelectionTimeoutMS',
+  'serverSelectionTryOnce',
+  'socketTimeoutMS',
+  'srvMaxHosts',
+  'srvServiceName',
+  'ssl',
+  'timeoutMS',
+  'tls',
+  'tlsAllowInvalidCertificates',
+  'tlsAllowInvalidHostnames',
+  'tlsCAFile',
+  'tlsCertificateKeyFile',
+  'tlsCertificateKeyFilePassword',
+  'tlsInsecure',
+  'w',
+  'waitQueueTimeoutMS',
+  'wTimeoutMS',
+  'zlibCompressionLevel',
+];
+
+/**
+ * Default settings.
+ *
+ * @type {{connectTimeoutMS: number}}
+ */
+const DEFAULT_SETTINGS = {
+  reconnectInterval: 2000, // adapter specific option
+  connectTimeoutMS: 2000,
+  serverSelectionTimeoutMS: 2000,
+};
+
+/**
+ * Mongodb adapter.
+ */
+export class MongodbAdapter extends Adapter {
+  /**
+   * Mongodb instance.
+   *
+   * @private
+   */
+  _client;
+
+  /**
+   * Collections.
+   *
+   * @type {Map<any, any>}
+   * @private
+   */
+  _collections = new Map();
+
+  /**
+   * Connected.
+   *
+   * @type {boolean}
+   * @private
+   */
+  _connected = false;
+
+  /**
+   * Connected.
+   *
+   * @return {boolean}
+   */
+  get connected() {
+    return this._connected;
+  }
+
+  /**
+   * Connecting.
+   *
+   * @type {boolean}
+   * @private
+   */
+  _connecting = false;
+
+  /**
+   * Connecting.
+   *
+   * @return {boolean}
+   */
+  get connecting() {
+    return this._connecting;
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param services
+   * @param settings
+   */
+  constructor(services, settings) {
+    settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
+    super(services, settings);
+    settings.protocol = settings.protocol || 'mongodb';
+    settings.hostname = settings.hostname || settings.host || '127.0.0.1';
+    settings.port = settings.port || 27017;
+    settings.database = settings.database || settings.db || 'test';
+  }
+
+  /**
+   * Connect.
+   *
+   * @return {Promise<*|undefined>}
+   * @private
+   */
+  async connect() {
+    if (this._connecting) {
+      const tryAgainAfter = 500;
+      await new Promise(r => setTimeout(() => r(), tryAgainAfter));
+      return this.connect();
+    }
+
+    if (this._connected) return;
+    this._connecting = true;
+
+    const options = selectObjectKeys(this.settings, MONGODB_OPTION_NAMES);
+    const url = createMongodbUrl(this.settings);
+
+    // console.log(`Connecting to ${url}`);
+    this._client = new MongoClient(url, options);
+
+    const {reconnectInterval} = this.settings;
+    const connectFn = async () => {
+      try {
+        await this._client.connect();
+      } catch (e) {
+        console.error(e);
+        // console.log('MongoDB connection failed!');
+        // console.log(`Reconnecting after ${reconnectInterval} ms.`);
+        await new Promise(r => setTimeout(() => r(), reconnectInterval));
+        return connectFn();
+      }
+      // console.log('MongoDB is connected.');
+      this._connected = true;
+      this._connecting = false;
+    };
+
+    await connectFn();
+
+    this._client.once('serverClosed', event => {
+      if (this._connected) {
+        this._connected = false;
+        // console.log('MongoDB lost connection!');
+        console.log(event);
+        // console.log(`Reconnecting after ${reconnectInterval} ms.`);
+        setTimeout(() => connectFn(), reconnectInterval);
+      } else {
+        // console.log('MongoDB connection closed.');
+      }
+    });
+  }
+
+  /**
+   * Disconnect.
+   *
+   * @return {Promise<*|undefined>}
+   */
+  async disconnect() {
+    if (this._connecting) {
+      const tryAgainAfter = 500;
+      await new Promise(r => setTimeout(() => r(), tryAgainAfter));
+      return this.disconnect();
+    }
+    if (!this._connected) return;
+    this._connected = false;
+    if (this._client) await this._client.close();
+  }
+
+  /**
+   * Get id prop name.
+   *
+   * @param modelName
+   */
+  _getIdPropName(modelName) {
+    return this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
+      modelName,
+    );
+  }
+
+  /**
+   * Get id col name.
+   *
+   * @param modelName
+   */
+  _getIdColName(modelName) {
+    return this.get(ModelDefinitionUtils).getPrimaryKeyAsColumnName(modelName);
+  }
+
+  /**
+   * Coerce id.
+   *
+   * @param value
+   * @return {ObjectId|*}
+   * @private
+   */
+  _coerceId(value) {
+    if (value == null) return value;
+    if (isObjectId(value)) return new ObjectId(value);
+    return value;
+  }
+
+  /**
+   * Coerce iso date.
+   *
+   * @param value
+   * @return {*|Date}
+   * @private
+   */
+  _coerceIsoDate(value) {
+    if (value === null) return value;
+    if (isIsoDate(value)) return new Date(value);
+    return value;
+  }
+
+  /**
+   * To database.
+   *
+   * @param {string} modelName
+   * @param {object} modelData
+   * @return {object}
+   * @private
+   */
+  _toDatabase(modelName, modelData) {
+    const tableData = this.get(
+      ModelDefinitionUtils,
+    ).convertPropertyNamesToColumnNames(modelName, modelData);
+
+    const idColName = this._getIdColName(modelName);
+    if (idColName !== 'id' && idColName !== '_id')
+      throw new InvalidArgumentError(
+        'MongoDB is not supporting custom names of the primary key. ' +
+          'Do use "id" as a primary key instead of %s.',
+        idColName,
+      );
+    if (idColName in tableData && idColName !== '_id') {
+      tableData._id = tableData[idColName];
+      delete tableData[idColName];
+    }
+
+    return transformValuesDeep(tableData, value => {
+      if (value instanceof ObjectId) return value;
+      if (value instanceof Date) return value;
+      if (isObjectId(value)) return new ObjectId(value);
+      if (isIsoDate(value)) return new Date(value);
+      return value;
+    });
+  }
+
+  /**
+   * From database.
+   *
+   * @param {string} modelName
+   * @param {object} tableData
+   * @return {object}
+   * @private
+   */
+  _fromDatabase(modelName, tableData) {
+    if ('_id' in tableData) {
+      const idColName = this._getIdColName(modelName);
+      if (idColName !== 'id' && idColName !== '_id')
+        throw new InvalidArgumentError(
+          'MongoDB is not supporting custom names of the primary key. ' +
+            'Do use "id" as a primary key instead of %s.',
+          idColName,
+        );
+      if (idColName !== '_id') {
+        tableData[idColName] = tableData._id;
+        delete tableData._id;
+      }
+    }
+
+    const modelData = this.get(
+      ModelDefinitionUtils,
+    ).convertColumnNamesToPropertyNames(modelName, tableData);
+
+    return transformValuesDeep(modelData, value => {
+      if (value instanceof ObjectId) return String(value);
+      if (value instanceof Date) return value.toISOString();
+      return value;
+    });
+  }
+
+  /**
+   * Get collection.
+   *
+   * @param {string} modelName
+   * @return {*}
+   * @private
+   */
+  _getCollection(modelName) {
+    let collection = this._collections.get(modelName);
+    if (collection) return collection;
+    const tableName =
+      this.get(ModelDefinitionUtils).getTableNameByModelName(modelName);
+    collection = this._client.db(this.settings.database).collection(tableName);
+    this._collections.set(modelName, collection);
+    return collection;
+  }
+
+  /**
+   * Get id type.
+   *
+   * @param modelName
+   * @return {string|*}
+   * @private
+   */
+  _getIdType(modelName) {
+    const utils = this.get(ModelDefinitionUtils);
+    const pkPropName = utils.getPrimaryKeyAsPropertyName(modelName);
+    return utils.getDataTypeByPropertyName(modelName, pkPropName);
+  }
+
+  /**
+   * Build projection.
+   *
+   * @param {string} modelName
+   * @param {string|string[]} fields
+   * @return {Record<string, number>|undefined}
+   * @private
+   */
+  _buildProjection(modelName, fields) {
+    if (!fields) return;
+    fields = Array.isArray(fields) ? fields : [fields];
+    if (!fields.length) return;
+    if (fields.indexOf('_id') === -1) fields.push('_id');
+    return fields.reduce((acc, field) => {
+      if (!field || typeof field !== 'string')
+        throw new InvalidArgumentError(
+          'A field name must be a non-empty String, but %s given.',
+          field,
+        );
+      let colName = this._getColName(modelName, field);
+      acc[colName] = 1;
+      return acc;
+    }, {});
+  }
+
+  /**
+   * Get col name.
+   *
+   * @param {string} modelName
+   * @param {string} propName
+   * @return {string}
+   * @private
+   */
+  _getColName(modelName, propName) {
+    if (!propName || typeof propName !== 'string')
+      throw new InvalidArgumentError(
+        'A property name must be a non-empty String, but %s given.',
+        propName,
+      );
+    const utils = this.get(ModelDefinitionUtils);
+    let colName = propName;
+    try {
+      colName = utils.getColumnNameByPropertyName(modelName, propName);
+    } catch (error) {
+      if (
+        !(error instanceof InvalidArgumentError) ||
+        error.message.indexOf('does not have the property') === -1
+      ) {
+        throw error;
+      }
+    }
+    return colName;
+  }
+
+  /**
+   * Build sort.
+   *
+   * @param {string} modelName
+   * @param {string|string[]} clause
+   * @return {object|undefined}
+   * @private
+   */
+  _buildSort(modelName, clause) {
+    if (!clause) return;
+    clause = Array.isArray(clause) ? clause : [clause];
+    if (!clause.length) return;
+    const utils = this.get(ModelDefinitionUtils);
+    const idPropName = this._getIdPropName(modelName);
+    return clause.reduce((acc, order) => {
+      if (!order || typeof order !== 'string')
+        throw new InvalidArgumentError(
+          'A field order must be a non-empty String, but %s given.',
+          order,
+        );
+      const direction = order.match(/\s+(A|DE)SC$/);
+      let key = order.replace(/\s+(A|DE)SC$/, '').trim();
+      if (key === idPropName) {
+        key = '_id';
+      } else {
+        try {
+          key = utils.getColumnNameByPropertyName(modelName, key);
+        } catch (error) {
+          if (
+            !(error instanceof InvalidArgumentError) ||
+            error.message.indexOf('does not have the property') === -1
+          ) {
+            throw error;
+          }
+        }
+      }
+      acc[key] = direction && direction[1] === 'DE' ? -1 : 1;
+      return acc;
+    }, {});
+  }
+
+  /**
+   * Build query.
+   *
+   * @param {string} modelName
+   * @param {object} clause
+   * @return {object}
+   * @private
+   */
+  _buildQuery(modelName, clause) {
+    const query = {};
+    if (!clause || typeof clause !== 'object') return query;
+    const idPropName = this._getIdPropName(modelName);
+    Object.keys(clause).forEach(key => {
+      let cond = clause[key];
+      // and/or/nor clause
+      if (key === 'and' || key === 'or' || key === 'nor') {
+        if (Array.isArray(cond))
+          cond = cond.map(c => this._buildQuery(modelName, c));
+        query['$' + key] = cond;
+        return;
+      }
+      // id
+      if (key === idPropName) {
+        key = '_id';
+      } else {
+        key = this._getColName(modelName, key);
+      }
+      // string
+      if (typeof cond === 'string') {
+        query[key] = this._coerceId(cond);
+        return;
+      }
+      // ObjectId
+      if (cond instanceof ObjectId) {
+        query[key] = cond;
+        return;
+      }
+      // operator
+      if (cond && cond.constructor && cond.constructor.name === 'Object') {
+        // eq
+        if ('eq' in cond) {
+          query[key] = this._coerceId(cond.eq);
+        }
+        // neq
+        if ('neq' in cond) {
+          query[key] = {$ne: this._coerceId(cond.neq)};
+        }
+        // gt
+        if ('gt' in cond) {
+          query[key] = {$gt: cond.gt};
+        }
+        // lt
+        if ('lt' in cond) {
+          query[key] = {$lt: cond.lt};
+        }
+        // gte
+        if ('gte' in cond) {
+          query[key] = {$gte: cond.gte};
+        }
+        // lte
+        if ('lte' in cond) {
+          query[key] = {$lte: cond.lte};
+        }
+        // inq
+        if ('inq' in cond) {
+          if (!cond.inq || !Array.isArray(cond.inq))
+            throw new InvalidOperatorValueError(
+              'inq',
+              'an Array of possible values',
+              cond.inq,
+            );
+          query[key] = {$in: cond.inq.map(v => this._coerceId(v))};
+        }
+        // nin
+        if ('nin' in cond) {
+          if (!cond.nin || !Array.isArray(cond.nin))
+            throw new InvalidOperatorValueError(
+              'nin',
+              'an Array of possible values',
+              cond,
+            );
+          query[key] = {$nin: cond.nin.map(v => this._coerceId(v))};
+        }
+        // between
+        if ('between' in cond) {
+          if (!Array.isArray(cond.between) || cond.between.length !== 2)
+            throw new InvalidOperatorValueError(
+              'between',
+              'an Array of 2 elements',
+              cond.between,
+            );
+          query[key] = {$gte: cond.between[0], $lte: cond.between[1]};
+        }
+        // exists
+        if ('exists' in cond) {
+          if (typeof cond.exists !== 'boolean')
+            throw new InvalidOperatorValueError(
+              'exists',
+              'a Boolean',
+              cond.exists,
+            );
+          query[key] = {$exists: cond.exists};
+        }
+        // like
+        if ('like' in cond) {
+          if (typeof cond.like !== 'string' && !(cond.like instanceof RegExp))
+            throw new InvalidOperatorValueError(
+              'like',
+              'a String or RegExp',
+              cond.like,
+            );
+          query[key] = {$regex: stringToRegexp(cond.like)};
+        }
+        // nlike
+        if ('nlike' in cond) {
+          if (typeof cond.nlike !== 'string' && !(cond.nlike instanceof RegExp))
+            throw new InvalidOperatorValueError(
+              'nlike',
+              'a String or RegExp',
+              cond.nlike,
+            );
+          query[key] = {$not: stringToRegexp(cond.nlike)};
+        }
+        // ilike
+        if ('ilike' in cond) {
+          if (typeof cond.ilike !== 'string' && !(cond.ilike instanceof RegExp))
+            throw new InvalidOperatorValueError(
+              'ilike',
+              'a String or RegExp',
+              cond.ilike,
+            );
+          query[key] = {$regex: stringToRegexp(cond.ilike, 'i')};
+        }
+        // nilike
+        if ('nilike' in cond) {
+          if (
+            typeof cond.nilike !== 'string' &&
+            !(cond.nilike instanceof RegExp)
+          ) {
+            throw new InvalidOperatorValueError(
+              'nilike',
+              'a String or RegExp',
+              cond.nilike,
+            );
+          }
+          query[key] = {$not: stringToRegexp(cond.nilike, 'i')};
+        }
+        // regexp and flags (optional)
+        if ('regexp' in cond) {
+          if (
+            typeof cond.regexp !== 'string' &&
+            !(cond.regexp instanceof RegExp)
+          ) {
+            throw new InvalidOperatorValueError(
+              'regexp',
+              'a String or RegExp',
+              cond.regexp,
+            );
+          }
+          const flags = cond.flags || undefined;
+          if (flags && typeof flags !== 'string')
+            throw new InvalidArgumentError(
+              'RegExp flags must be a String, but %s given.',
+              cond.flags,
+            );
+          query[key] = {$regex: stringToRegexp(cond.regexp, flags)};
+        }
+        return;
+      }
+      // unknown
+      query[key] = cond;
+    });
+    return query;
+  }
+
+  /**
+   * Create.
+   *
+   * @param {string} modelName
+   * @param {object} modelData
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async create(modelName, modelData, filter = undefined) {
+    await this.connect();
+    const idPropName = this._getIdPropName(modelName);
+    const idValue = modelData[idPropName];
+    if (idValue == null) {
+      const pkType = this._getIdType(modelName);
+      if (pkType !== DataType.STRING && pkType !== DataType.ANY)
+        throw new InvalidArgumentError(
+          'MongoDB unable to generate primary keys of %s. ' +
+            'Do provide your own value for the %s property ' +
+            'or set property type to String.',
+          new String(capitalize(pkType)),
+          idPropName,
+        );
+      delete modelData[idPropName];
+    }
+    const tableData = this._toDatabase(modelName, modelData);
+    const table = this._getCollection(modelName);
+    const {insertedId} = await table.insertOne(tableData);
+    const projection = this._buildProjection(
+      modelName,
+      filter && filter.fields,
+    );
+    const insertedData = await table.findOne({_id: insertedId}, {projection});
+    return this._fromDatabase(modelName, insertedData);
+  }
+
+  /**
+   * Replace by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {object} modelData
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async replaceById(modelName, id, modelData, filter = undefined) {
+    await this.connect();
+    id = this._coerceId(id);
+    const idPropName = this._getIdPropName(modelName);
+    modelData[idPropName] = id;
+    const tableData = this._toDatabase(modelName, modelData);
+    const table = this._getCollection(modelName);
+    const {modifiedCount} = await table.replaceOne({_id: id}, tableData);
+    if (modifiedCount < 1)
+      throw new InvalidArgumentError('Identifier %s is not found.', String(id));
+    const projection = this._buildProjection(
+      modelName,
+      filter && filter.fields,
+    );
+    const replacedData = await table.findOne({_id: id}, {projection});
+    return this._fromDatabase(modelName, replacedData);
+  }
+
+  /**
+   * Patch by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {object} modelData
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async patchById(modelName, id, modelData, filter = undefined) {
+    await this.connect();
+    id = this._coerceId(id);
+    const idPropName = this._getIdPropName(modelName);
+    delete modelData[idPropName];
+    const tableData = this._toDatabase(modelName, modelData);
+    const table = this._getCollection(modelName);
+    const {modifiedCount} = await table.updateOne({_id: id}, {$set: tableData});
+    if (modifiedCount < 1)
+      throw new InvalidArgumentError('Identifier %s is not found.', String(id));
+    const projection = this._buildProjection(
+      modelName,
+      filter && filter.fields,
+    );
+    const patchedData = await table.findOne({_id: id}, {projection});
+    return this._fromDatabase(modelName, patchedData);
+  }
+
+  /**
+   * Find.
+   *
+   * @param {string} modelName
+   * @param {object|undefined} filter
+   * @return {Promise<object[]>}
+   */
+  async find(modelName, filter = undefined) {
+    await this.connect();
+    filter = filter || {};
+    const query = this._buildQuery(modelName, filter.where);
+    const sort = this._buildSort(modelName, filter.order);
+    const limit = filter.limit || undefined;
+    const skip = filter.skip || undefined;
+    const projection = this._buildProjection(modelName, filter.fields);
+    const collection = this._getCollection(modelName);
+    const options = {sort, limit, skip, projection};
+    const tableItems = await collection.find(query, options).toArray();
+    return tableItems.map(v => this._fromDatabase(modelName, v));
+  }
+
+  /**
+   * Find by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async findById(modelName, id, filter = undefined) {
+    await this.connect();
+    id = this._coerceId(id);
+    const table = this._getCollection(modelName);
+    const projection = this._buildProjection(
+      modelName,
+      filter && filter.fields,
+    );
+    const patchedData = await table.findOne({_id: id}, {projection});
+    if (!patchedData)
+      throw new InvalidArgumentError('Identifier %s is not found.', String(id));
+    return this._fromDatabase(modelName, patchedData);
+  }
+
+  /**
+   * Delete.
+   *
+   * @param {string} modelName
+   * @param {object|undefined} where
+   * @return {Promise<number>}
+   */
+  async delete(modelName, where = undefined) {
+    await this.connect();
+    const table = this._getCollection(modelName);
+    const query = this._buildQuery(modelName, where);
+    const {deletedCount} = await table.deleteMany(query);
+    return deletedCount;
+  }
+
+  /**
+   * Delete by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @return {Promise<boolean>}
+   */
+  async deleteById(modelName, id) {
+    await this.connect();
+    id = this._coerceId(id);
+    const table = this._getCollection(modelName);
+    const {deletedCount} = await table.deleteOne({_id: id});
+    return deletedCount > 0;
+  }
+
+  /**
+   * Exists.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @return {Promise<boolean>}
+   */
+  async exists(modelName, id) {
+    await this.connect();
+    id = this._coerceId(id);
+    const table = this._getCollection(modelName);
+    const result = await table.findOne({_id: id}, {});
+    return result != null;
+  }
+
+  /**
+   * Count.
+   *
+   * @param {string} modelName
+   * @param {object|undefined} where
+   * @return {Promise<number>}
+   */
+  async count(modelName, where = undefined) {
+    await this.connect();
+    const query = this._buildQuery(modelName, where);
+    const table = this._getCollection(modelName);
+    return await table.count(query);
+  }
+}

+ 2391 - 0
src/mongodb-adapter.spec.js

@@ -0,0 +1,2391 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {ObjectId} from 'mongodb';
+import {MongoClient} from 'mongodb';
+import {Schema} from '@e22m4u/repository';
+import {Service} from '@e22m4u/repository';
+import {DataType} from '@e22m4u/repository';
+import {createMongodbUrl} from './utils/index.js';
+import {MongodbAdapter} from './mongodb-adapter.js';
+import {AdapterRegistry} from '@e22m4u/repository';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '@e22m4u/repository';
+
+const CONFIG = {
+  host: process.env.MONGODB_HOST || 'localhost',
+  port: process.env.MONGODB_PORT || 27017,
+  database: process.env.MONGODB_DATABASE,
+};
+
+const MDB_CLIENT = new MongoClient(createMongodbUrl(CONFIG));
+const ADAPTERS_STACK = [];
+
+function createSchema() {
+  const schema = new Schema();
+  const adapter = new MongodbAdapter(schema._services, CONFIG);
+  ADAPTERS_STACK.push(adapter);
+  schema.defineDatasource({name: 'mongodb', adapter: 'mongodb'});
+  schema.get(AdapterRegistry)._adapters['mongodb'] = adapter;
+  return schema;
+}
+
+describe('MongodbAdapter', function () {
+  this.timeout(15000);
+
+  afterEach(async function () {
+    await MDB_CLIENT.db(CONFIG.database).dropDatabase();
+  });
+
+  after(async function () {
+    for await (const adapter of ADAPTERS_STACK) {
+      await adapter.disconnect();
+    }
+    await MDB_CLIENT.close(true);
+  });
+
+  it('able to connect and disconnect', async function () {
+    const S = new Service();
+    const adapter = new MongodbAdapter(S._services, CONFIG);
+    await adapter.connect();
+    expect(adapter.connected).to.be.true;
+    await adapter.disconnect();
+  });
+
+  describe('create', function () {
+    it('generates a new identifier when a value of a primary key is not provided', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: 'bar'});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 'bar'});
+      expect(typeof result[DEF_PK]).to.be.eq('string');
+      expect(result[DEF_PK]).to.have.lengthOf(24);
+    });
+
+    it('generates a new identifier when a value of a primary key is undefined', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({[DEF_PK]: undefined, foo: 'bar'});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 'bar'});
+      expect(typeof result[DEF_PK]).to.be.eq('string');
+      expect(result[DEF_PK]).to.have.lengthOf(24);
+    });
+
+    it('generates a new identifier when a value of a primary key is null', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({[DEF_PK]: null, foo: 'bar'});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 'bar'});
+      expect(typeof result[DEF_PK]).to.be.eq('string');
+      expect(result[DEF_PK]).to.have.lengthOf(24);
+    });
+
+    it('generates a new identifier for a primary key of a "string" type', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const result = await rep.create({[DEF_PK]: null, foo: 'bar'});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 'bar'});
+      expect(typeof result[DEF_PK]).to.be.eq('string');
+      expect(result[DEF_PK]).to.have.lengthOf(24);
+    });
+
+    it('generates a new identifier for a primary key of a "any" type', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const result = await rep.create({[DEF_PK]: null, foo: 'bar'});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 'bar'});
+      expect(typeof result[DEF_PK]).to.be.eq('string');
+      expect(result[DEF_PK]).to.have.lengthOf(24);
+    });
+
+    it('throws an error when generating a new value for a primary key of a "number" type', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const promise = rep.create({});
+      expect(promise).to.be.rejectedWith(
+        'MongoDB unable to generate primary keys of Number. ' +
+          'Do provide your own value for the "id" property ' +
+          'or set the property type to String.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of a "boolean" type', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.BOOLEAN,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const promise = rep.create({});
+      expect(promise).to.be.rejectedWith(
+        'MongoDB unable to generate primary keys of Boolean. ' +
+          'Do provide your own value for the "id" property ' +
+          'or set the property type to String.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of a "array" type', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.ARRAY,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const promise = rep.create({});
+      expect(promise).to.be.rejectedWith(
+        'MongoDB unable to generate primary keys of Array. ' +
+          'Do provide your own value for the "id" property ' +
+          'or set the property type to String.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of a "object" type', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.OBJECT,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const promise = rep.create({});
+      expect(promise).to.be.rejectedWith(
+        'MongoDB unable to generate primary keys of Object. ' +
+          'Do provide your own value for the "id" property ' +
+          'or set the property type to String.',
+      );
+    });
+
+    it('allows to specify an ObjectID instance for a default primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const result = await rep.create({[DEF_PK]: oid});
+      expect(result).to.be.eql({[DEF_PK]: String(oid)});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.not.null;
+    });
+
+    it('allows to specify an ObjectID string for a default primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const id = String(oid);
+      const result = await rep.create({[DEF_PK]: id});
+      expect(result).to.be.eql({[DEF_PK]: id});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.not.null;
+    });
+
+    it('allows to specify an ObjectID instance for "id" primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const result = await rep.create({id: oid});
+      expect(result).to.be.eql({id: String(oid)});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.not.null;
+    });
+
+    it('allows to specify an ObjectID string for "id" primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const id = String(oid);
+      const result = await rep.create({id});
+      expect(result).to.be.eql({id});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.not.null;
+    });
+
+    it('allows to specify an ObjectID instance for "_id" primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          _id: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const result = await rep.create({_id: oid});
+      expect(result).to.be.eql({_id: String(oid)});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.not.null;
+    });
+
+    it('allows to specify an ObjectID string for "_id" primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          _id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const id = String(oid);
+      const result = await rep.create({_id: id});
+      expect(result).to.be.eql({_id: id});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.not.null;
+    });
+
+    it('throws an error for a custom primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          myId: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const promise = rep.create({myId: oid});
+      await expect(promise).to.be.rejectedWith(
+        'MongoDB is not supporting custom names of the primary key. ' +
+          'Do use "id" as a primary key instead of "myId".',
+      );
+    });
+
+    it('throws an error if a given "number" identifier already exists', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      await rep.create({[DEF_PK]: 10});
+      const promise = rep.create({[DEF_PK]: 10});
+      await expect(promise).to.be.rejectedWith(
+        'E11000 duplicate key error collection: test.model index: ' +
+          '_id_ dup key: { _id: 10 }',
+      );
+    });
+
+    it('throws an error if a given "string" identifier already exists', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      await rep.create({[DEF_PK]: 'str'});
+      const promise = rep.create({[DEF_PK]: 'str'});
+      await expect(promise).to.be.rejectedWith(
+        'E11000 duplicate key error collection: test.model index: ' +
+          '_id_ dup key: { _id: "str" }',
+      );
+    });
+
+    it('throws an error if a given ObjectId instance identifier already exists', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      await rep.create({[DEF_PK]: oid});
+      const promise = rep.create({[DEF_PK]: oid});
+      await expect(promise).to.be.rejectedWith(
+        format(
+          'E11000 duplicate key error collection: test.model index: ' +
+            "_id_ dup key: { _id: ObjectId('%s') }",
+          oid,
+        ),
+      );
+    });
+
+    it('throws an error if a given ObjectId string identifier already exists', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const id = String(oid);
+      await rep.create({[DEF_PK]: id});
+      const promise = rep.create({[DEF_PK]: id});
+      await expect(promise).to.be.rejectedWith(
+        format(
+          'E11000 duplicate key error collection: test.model index: ' +
+            "_id_ dup key: { _id: ObjectId('%s') }",
+          id,
+        ),
+      );
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: 10});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 10});
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, bar: 10});
+    });
+
+    it('uses a specified column name for a regular property with a default value', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+            default: 10,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const result = await rep.create({});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 10});
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, bar: 10});
+    });
+
+    it('stores a Date instance as date and returns string type', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const date = new Date();
+      const dateString = date.toISOString();
+      const result = await rep.create({date});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], date: dateString});
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, date});
+    });
+
+    it('stores a Date string as date and returns string type', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const date = new Date();
+      const dateString = date.toISOString();
+      const result = await rep.create({date: dateString});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], date: dateString});
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, date});
+    });
+
+    it('stores a string as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: 'str'});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 'str'});
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 'str'});
+    });
+
+    it('stores a number as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: 10});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 10});
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 10});
+    });
+
+    it('stores a boolean as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: true, bar: false});
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: true,
+        bar: false,
+      });
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: true, bar: false});
+    });
+
+    it('stores an array as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: ['bar']});
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: ['bar'],
+      });
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: ['bar']});
+    });
+
+    it('stores an object as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: {bar: 10}});
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: {bar: 10},
+      });
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: {bar: 10}});
+    });
+
+    it('stores an undefined as null', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: undefined});
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: null,
+      });
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: null});
+    });
+
+    it('stores an null as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: null});
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: null,
+      });
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: null});
+    });
+
+    it('uses a short fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create({foo: 10, bar: 20}, {fields: 'foo'});
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 10});
+    });
+
+    it('uses a full fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.create(
+        {foo: 10, bar: 20, baz: 30},
+        {fields: ['foo', 'bar']},
+      );
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: 10, bar: 20});
+      const oid = new ObjectId(result[DEF_PK]);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 10, bar: 20, baz: 30});
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.NUMBER,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const result = await rep.create(
+        {foo: 10, bar: 20, baz: 30},
+        {fields: ['fooCol', 'barCol']},
+      );
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK]});
+    });
+  });
+
+  describe('replaceById', function () {
+    it('removes properties when replacing an item by a given identifier', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {bar: 20});
+      expect(replaced).to.be.eql({[DEF_PK]: id, bar: 20});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, bar: 20});
+    });
+
+    it('ignores identifier value in a given data in case of a default primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      await rep.create({[DEF_PK]: 'foo', prop: 10});
+      const replaced = await rep.replaceById('foo', {
+        [DEF_PK]: 'bar',
+        prop: 20,
+      });
+      expect(replaced).to.be.eql({[DEF_PK]: 'foo', prop: 20});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: 'foo'});
+      expect(rawData).to.be.eql({_id: 'foo', prop: 20});
+    });
+
+    it('ignores identifier value in a given data in case of a custom primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          myId: {
+            type: DataType.STRING,
+            primaryKey: true,
+            columnName: '_id',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      await rep.create({myId: 'foo', prop: 10});
+      const replaced = await rep.replaceById('foo', {myId: 'bar', prop: 20});
+      expect(replaced).to.be.eql({myId: 'foo', prop: 20});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: 'foo'});
+      expect(rawData).to.be.eql({_id: 'foo', prop: 20});
+    });
+
+    it('throws an error if a given identifier does not exist', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const promise = rep.replaceById(oid, {foo: 10});
+      await expect(promise).to.be.rejectedWith(
+        format('Identifier "%s" is not found.', oid),
+      );
+    });
+
+    it('throws an error for a custom primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          myId: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const promise = rep.replaceById('id', {foo: 10});
+      await expect(promise).to.be.rejectedWith(
+        'MongoDB is not supporting custom names of the primary key. ' +
+          'Do use "id" as a primary key instead of "myId".',
+      );
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: 20});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: 20});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, bar: 20});
+    });
+
+    it('stores a Date instance as date and returns string type', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const date = new Date();
+      const dateString = date.toISOString();
+      const created = await rep.create({date: null});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {date});
+      expect(replaced).to.be.eql({[DEF_PK]: id, date: dateString});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, date});
+    });
+
+    it('stores a Date string as date and returns string type', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const date = new Date();
+      const dateString = date.toISOString();
+      const created = await rep.create({date: null});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {date: dateString});
+      expect(replaced).to.be.eql({[DEF_PK]: id, date: dateString});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, date});
+    });
+
+    it('stores a string as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: 'str'});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: 'str'});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 'str'});
+    });
+
+    it('stores a number as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: 10});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: 10});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 10});
+    });
+
+    it('stores a boolean as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null, bar: null});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: true, bar: false});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: true, bar: false});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: true, bar: false});
+    });
+
+    it('stores an array as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: ['bar']});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: ['bar']});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: ['bar']});
+    });
+
+    it('stores an object as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: {bar: 10}});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: {bar: 10}});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: {bar: 10}});
+    });
+
+    it('stores an undefined as null', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: undefined});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: null});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: null});
+    });
+
+    it('stores an null as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(id, {foo: null});
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: null});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: null});
+    });
+
+    it('uses a short fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10, bar: 20});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(
+        id,
+        {foo: 15, bar: 25},
+        {fields: 'foo'},
+      );
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: 15});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 15, bar: 25});
+    });
+
+    it('uses a full fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10, bar: 20, baz: 30});
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(
+        id,
+        {foo: 15, bar: 25, baz: 35},
+        {fields: ['foo', 'bar']},
+      );
+      expect(replaced).to.be.eql({[DEF_PK]: id, foo: 15, bar: 25});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 15, bar: 25, baz: 35});
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.NUMBER,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const created = await rep.create(
+        {foo: 10, bar: 20, baz: 30},
+        {fields: ['fooCol', 'barCol']},
+      );
+      const id = created[DEF_PK];
+      const replaced = await rep.replaceById(
+        id,
+        {foo: 15, bar: 25, baz: 35},
+        {fields: ['fooCol', 'barCol']},
+      );
+      expect(replaced).to.be.eql({[DEF_PK]: replaced[DEF_PK]});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, fooCol: 15, barCol: 25, bazCol: 35});
+    });
+  });
+
+  describe('patchById', function () {
+    it('updates only provided properties by a given identifier', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {bar: 20});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: 10, bar: 20});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 10, bar: 20});
+    });
+
+    it('does not throw an error if a partial data does not have required property', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            required: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const {insertedId: oid} = await MDB_CLIENT.db()
+        .collection('model')
+        .insertOne({bar: 10});
+      const patched = await rep.patchById(oid, {baz: 20});
+      const id = String(oid);
+      expect(patched).to.be.eql({[DEF_PK]: id, bar: 10, baz: 20});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, bar: 10, baz: 20});
+    });
+
+    it('ignores identifier value in a given data in case of a default primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      await rep.create({[DEF_PK]: 'foo', prop: 10});
+      const patched = await rep.patchById('foo', {
+        [DEF_PK]: 'bar',
+        prop: 20,
+      });
+      expect(patched).to.be.eql({[DEF_PK]: 'foo', prop: 20});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: 'foo'});
+      expect(rawData).to.be.eql({_id: 'foo', prop: 20});
+    });
+
+    it('ignores identifier value in a given data in case of a custom primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          myId: {
+            type: DataType.STRING,
+            primaryKey: true,
+            columnName: '_id',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      await rep.create({myId: 'foo', prop: 10});
+      const patched = await rep.patchById('foo', {myId: 'bar', prop: 20});
+      expect(patched).to.be.eql({myId: 'foo', prop: 20});
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: 'foo'});
+      expect(rawData).to.be.eql({_id: 'foo', prop: 20});
+    });
+
+    it('throws an error if a given identifier does not exist', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const promise = rep.patchById(oid, {foo: 10});
+      await expect(promise).to.be.rejectedWith(
+        format('Identifier "%s" is not found.', oid),
+      );
+    });
+
+    it('throws an error for a custom primary key', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          myId: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const promise = rep.patchById('id', {foo: 10});
+      await expect(promise).to.be.rejectedWith(
+        'MongoDB is not supporting custom names of the primary key. ' +
+          'Do use "id" as a primary key instead of "myId".',
+      );
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const patched = await rep.replaceById(id, {foo: 20});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: 20});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, bar: 20});
+    });
+
+    it('stores a Date instance as date and returns string type', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const date = new Date();
+      const dateString = date.toISOString();
+      const created = await rep.create({date: null});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {date});
+      expect(patched).to.be.eql({[DEF_PK]: id, date: dateString});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, date});
+    });
+
+    it('stores a Date string as date and returns string type', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const date = new Date();
+      const dateString = date.toISOString();
+      const created = await rep.create({date: null});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {date: dateString});
+      expect(patched).to.be.eql({[DEF_PK]: id, date: dateString});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, date});
+    });
+
+    it('stores a string as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {foo: 'str'});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: 'str'});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 'str'});
+    });
+
+    it('stores a number as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {foo: 10});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: 10});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 10});
+    });
+
+    it('stores a boolean as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null, bar: null});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {foo: true, bar: false});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: true, bar: false});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: true, bar: false});
+    });
+
+    it('stores an array as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {foo: ['bar']});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: ['bar']});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: ['bar']});
+    });
+
+    it('stores an object as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: null});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {foo: {bar: 10}});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: {bar: 10}});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: {bar: 10}});
+    });
+
+    it('stores an undefined as null', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {foo: undefined});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: null});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: null});
+    });
+
+    it('stores an null as is', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(id, {foo: null});
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: null});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: null});
+    });
+
+    it('uses a short fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10, bar: 20});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(
+        id,
+        {foo: 15, bar: 25},
+        {fields: 'foo'},
+      );
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: 15});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 15, bar: 25});
+    });
+
+    it('uses a full fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10, bar: 20, baz: 30});
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(
+        id,
+        {foo: 15, bar: 25, baz: 35},
+        {fields: ['foo', 'bar']},
+      );
+      expect(patched).to.be.eql({[DEF_PK]: id, foo: 15, bar: 25});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, foo: 15, bar: 25, baz: 35});
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.NUMBER,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const created = await rep.create(
+        {foo: 10, bar: 20, baz: 30},
+        {fields: ['fooCol', 'barCol']},
+      );
+      const id = created[DEF_PK];
+      const patched = await rep.patchById(
+        id,
+        {foo: 15, bar: 25, baz: 35},
+        {fields: ['fooCol', 'barCol']},
+      );
+      expect(patched).to.be.eql({[DEF_PK]: patched[DEF_PK]});
+      const oid = new ObjectId(id);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .findOne({_id: oid});
+      expect(rawData).to.be.eql({_id: oid, fooCol: 15, barCol: 25, bazCol: 35});
+    });
+  });
+
+  describe('find', function () {
+    it('returns an empty array', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.find();
+      expect(result).to.be.empty;
+      expect(result).to.be.instanceOf(Array);
+    });
+
+    it('returns all items', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const doc1 = await rep.create({foo: 1});
+      const doc2 = await rep.create({bar: 2});
+      const doc3 = await rep.create({baz: 3});
+      const result = await rep.find();
+      expect(result).to.be.eql([doc1, doc2, doc3]);
+    });
+
+    it('uses a short fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created1 = await rep.create({foo: 10, bar: 20});
+      const created2 = await rep.create({foo: 20, bar: 30});
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const result = await rep.find({fields: 'foo'});
+      expect(result).to.be.eql([
+        {[DEF_PK]: id1, foo: 10},
+        {[DEF_PK]: id2, foo: 20},
+      ]);
+    });
+
+    it('uses a full fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created1 = await rep.create({foo: 10, bar: 20, baz: 30});
+      const created2 = await rep.create({foo: 20, bar: 30, baz: 40});
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const result = await rep.find({fields: ['foo', 'bar']});
+      expect(result).to.be.eql([
+        {[DEF_PK]: id1, foo: 10, bar: 20},
+        {[DEF_PK]: id2, foo: 20, bar: 30},
+      ]);
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.NUMBER,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const created1 = await rep.create({foo: 10, bar: 20, baz: 30});
+      const created2 = await rep.create({foo: 20, bar: 30, baz: 40});
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const result = await rep.find({fields: ['fooCol', 'barCol']});
+      expect(result).to.be.eql([{[DEF_PK]: id1}, {[DEF_PK]: id2}]);
+    });
+
+    it('uses a short order clause to sort results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created1 = await rep.create({foo: 20});
+      const created2 = await rep.create({foo: 5});
+      const created3 = await rep.create({foo: 10});
+      const result1 = await rep.find({order: 'foo'});
+      const result2 = await rep.find({order: 'foo ASC'});
+      const result3 = await rep.find({order: 'foo DESC'});
+      expect(result1).to.be.eql([
+        {[DEF_PK]: created2[DEF_PK], foo: 5},
+        {[DEF_PK]: created3[DEF_PK], foo: 10},
+        {[DEF_PK]: created1[DEF_PK], foo: 20},
+      ]);
+      expect(result2).to.be.eql([
+        {[DEF_PK]: created2[DEF_PK], foo: 5},
+        {[DEF_PK]: created3[DEF_PK], foo: 10},
+        {[DEF_PK]: created1[DEF_PK], foo: 20},
+      ]);
+      expect(result3).to.be.eql([
+        {[DEF_PK]: created1[DEF_PK], foo: 20},
+        {[DEF_PK]: created3[DEF_PK], foo: 10},
+        {[DEF_PK]: created2[DEF_PK], foo: 5},
+      ]);
+    });
+
+    it('uses a full order clause to sort results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created1 = await rep.create({foo: 20, bar: 'b'});
+      const created2 = await rep.create({foo: 5, bar: 'b'});
+      const created3 = await rep.create({foo: 10, bar: 'a'});
+      const result1 = await rep.find({order: ['bar DESC', 'foo']});
+      const result2 = await rep.find({order: ['bar', 'foo ASC']});
+      expect(result1).to.be.eql([
+        {[DEF_PK]: created2[DEF_PK], foo: 5, bar: 'b'},
+        {[DEF_PK]: created1[DEF_PK], foo: 20, bar: 'b'},
+        {[DEF_PK]: created3[DEF_PK], foo: 10, bar: 'a'},
+      ]);
+      expect(result2).to.be.eql([
+        {[DEF_PK]: created3[DEF_PK], foo: 10, bar: 'a'},
+        {[DEF_PK]: created2[DEF_PK], foo: 5, bar: 'b'},
+        {[DEF_PK]: created1[DEF_PK], foo: 20, bar: 'b'},
+      ]);
+    });
+
+    it('an order clause uses property names instead of column names', async function () {
+      const schema = createSchema();
+      schema.defineModel({
+        name: 'model',
+        datasource: 'mongodb',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.STRING,
+            columnName: 'barCol',
+          },
+        },
+      });
+      const rep = schema.getRepository('model');
+      const created1 = await rep.create({foo: 20, bar: 'b'});
+      const created2 = await rep.create({foo: 5, bar: 'b'});
+      const created3 = await rep.create({foo: 10, bar: 'a'});
+      const result1 = await rep.find({order: ['bar DESC', 'foo']});
+      const result2 = await rep.find({order: ['bar', 'foo ASC']});
+      expect(result1).to.be.eql([
+        {[DEF_PK]: created2[DEF_PK], foo: 5, bar: 'b'},
+        {[DEF_PK]: created1[DEF_PK], foo: 20, bar: 'b'},
+        {[DEF_PK]: created3[DEF_PK], foo: 10, bar: 'a'},
+      ]);
+      expect(result2).to.be.eql([
+        {[DEF_PK]: created3[DEF_PK], foo: 10, bar: 'a'},
+        {[DEF_PK]: created2[DEF_PK], foo: 5, bar: 'b'},
+        {[DEF_PK]: created1[DEF_PK], foo: 20, bar: 'b'},
+      ]);
+    });
+
+    it('uses a limit clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created1 = await rep.create({foo: 10});
+      const created2 = await rep.create({foo: 20});
+      await rep.create({foo: 30});
+      const result = await rep.find({limit: 2});
+      expect(result).to.be.eql([created1, created2]);
+    });
+
+    it('uses a skip clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      await rep.create({foo: 10});
+      const created2 = await rep.create({foo: 20});
+      const created3 = await rep.create({foo: 30});
+      const result = await rep.find({skip: 1});
+      expect(result).to.be.eql([created2, created3]);
+    });
+
+    describe('uses a where clause to filter results', function () {
+      it('matches by a document subset', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5, bar: 'b'});
+        const created2 = await rep.create({foo: 10, bar: 'a'});
+        const result1 = await rep.find({where: {foo: 10}});
+        const result2 = await rep.find({where: {foo: 5, bar: 'b'}});
+        expect(result1).to.be.eql([
+          {
+            [DEF_PK]: created2[DEF_PK],
+            foo: 10,
+            bar: 'a',
+          },
+        ]);
+        expect(result2).to.be.eql([
+          {
+            [DEF_PK]: created1[DEF_PK],
+            foo: 5,
+            bar: 'b',
+          },
+        ]);
+      });
+
+      it('matches by the "eq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 10});
+        const result = await rep.find({where: {foo: {eq: 10}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: 10},
+          {[DEF_PK]: created3[DEF_PK], foo: 10},
+        ]);
+      });
+
+      it('matches by the "neq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 10});
+        const result = await rep.find({where: {foo: {neq: 10}}});
+        expect(result).to.be.eql([{[DEF_PK]: created1[DEF_PK], foo: 5}]);
+      });
+
+      it('matches by the "gt" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 15});
+        const result = await rep.find({where: {foo: {gt: 10}}});
+        expect(result).to.be.eql([{[DEF_PK]: created3[DEF_PK], foo: 15}]);
+      });
+
+      it('matches by the "lt" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.find({where: {foo: {lt: 10}}});
+        expect(result).to.be.eql([{[DEF_PK]: created1[DEF_PK], foo: 5}]);
+      });
+
+      it('matches by the "gte" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 15});
+        const result = await rep.find({where: {foo: {gte: 10}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: 10},
+          {[DEF_PK]: created3[DEF_PK], foo: 15},
+        ]);
+      });
+
+      it('matches by the "lte" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.find({where: {foo: {lte: 10}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created1[DEF_PK], foo: 5},
+          {[DEF_PK]: created2[DEF_PK], foo: 10},
+        ]);
+      });
+
+      it('matches by the "inq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.find({where: {foo: {inq: [5, 10]}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created1[DEF_PK], foo: 5},
+          {[DEF_PK]: created2[DEF_PK], foo: 10},
+        ]);
+      });
+
+      it('matches by the "nin" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 15});
+        const result = await rep.find({where: {foo: {nin: [5, 10]}}});
+        expect(result).to.be.eql([{[DEF_PK]: created3[DEF_PK], foo: 15}]);
+      });
+
+      it('matches by the "between" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 15});
+        await rep.create({foo: 20});
+        const result = await rep.find({where: {foo: {between: [9, 16]}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: 10},
+          {[DEF_PK]: created3[DEF_PK], foo: 15},
+        ]);
+      });
+
+      it('matches by the "exists" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({});
+        const created2 = await rep.create({foo: undefined});
+        const created3 = await rep.create({foo: null});
+        const created4 = await rep.create({foo: 10});
+        const result1 = await rep.find({where: {foo: {exists: true}}});
+        const result2 = await rep.find({where: {foo: {exists: false}}});
+        expect(result1).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: null},
+          {[DEF_PK]: created3[DEF_PK], foo: null},
+          {[DEF_PK]: created4[DEF_PK], foo: 10},
+        ]);
+        expect(result2).to.be.eql([{[DEF_PK]: created1[DEF_PK]}]);
+      });
+
+      it('matches by the "like" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        const created2 = await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        const created4 = await rep.create({foo: 'sit amet'});
+        const result = await rep.find({where: {foo: {like: 'sit amet'}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: 'dolor sit amet'},
+          {[DEF_PK]: created4[DEF_PK], foo: 'sit amet'},
+        ]);
+      });
+
+      it('matches by the "ilike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        const created2 = await rep.create({foo: 'dolor sit amet'});
+        const created3 = await rep.create({foo: 'DOLOR SIT AMET'});
+        const created4 = await rep.create({foo: 'sit amet'});
+        const result = await rep.find({where: {foo: {ilike: 'sit amet'}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: 'dolor sit amet'},
+          {[DEF_PK]: created3[DEF_PK], foo: 'DOLOR SIT AMET'},
+          {[DEF_PK]: created4[DEF_PK], foo: 'sit amet'},
+        ]);
+      });
+
+      it('matches by the "nlike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        const created3 = await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.find({where: {foo: {nlike: 'sit amet'}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created1[DEF_PK], foo: 'lorem ipsum'},
+          {[DEF_PK]: created3[DEF_PK], foo: 'DOLOR SIT AMET'},
+        ]);
+      });
+
+      it('matches by the "nilike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.find({where: {foo: {nilike: 'sit amet'}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created1[DEF_PK], foo: 'lorem ipsum'},
+        ]);
+      });
+
+      it('matches by the "regexp" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        const created2 = await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        const created4 = await rep.create({foo: 'sit amet'});
+        const result = await rep.find({where: {foo: {regexp: 'sit am+'}}});
+        expect(result).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: 'dolor sit amet'},
+          {[DEF_PK]: created4[DEF_PK], foo: 'sit amet'},
+        ]);
+      });
+
+      it('matches by the "regexp" operator with flags', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        const created2 = await rep.create({foo: 'dolor sit amet'});
+        const created3 = await rep.create({foo: 'DOLOR SIT AMET'});
+        const created4 = await rep.create({foo: 'sit amet'});
+        const result = await rep.find({
+          where: {
+            foo: {regexp: 'sit am+', flags: 'i'},
+          },
+        });
+        expect(result).to.be.eql([
+          {[DEF_PK]: created2[DEF_PK], foo: 'dolor sit amet'},
+          {[DEF_PK]: created3[DEF_PK], foo: 'DOLOR SIT AMET'},
+          {[DEF_PK]: created4[DEF_PK], foo: 'sit amet'},
+        ]);
+      });
+    });
+  });
+
+  describe('findById', function () {
+    it('throws an error if a given identifier does not exist', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const promise = rep.findById(oid);
+      await expect(promise).to.be.rejectedWith(
+        format('Identifier "%s" is not found.', oid),
+      );
+    });
+
+    it('uses a short fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10, bar: 20});
+      const id = created[DEF_PK];
+      const result = await rep.findById(id, {fields: 'foo'});
+      expect(result).to.be.eql({[DEF_PK]: id, foo: 10});
+    });
+
+    it('uses a full fields clause to filter results', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const created = await rep.create({foo: 10, bar: 20, baz: 30});
+      const id = created[DEF_PK];
+      const result = await rep.findById(id, {fields: ['foo', 'bar']});
+      expect(result).to.be.eql({[DEF_PK]: id, foo: 10, bar: 20});
+    });
+  });
+
+  describe('delete', function () {
+    it('removes all table items and returns their number', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      await rep.create({foo: 1});
+      await rep.create({foo: 2});
+      await rep.create({foo: 3});
+      const result = await rep.delete();
+      expect(result).to.be.eql(3);
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .find()
+        .toArray();
+      expect(rawData).to.be.empty;
+    });
+
+    describe('removes by a where clause', function () {
+      it('removes by a document subset', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        const result = await rep.delete({foo: 10});
+        expect(result).to.be.eq(1);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 5},
+        ]);
+      });
+
+      it('matches by the "eq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 10});
+        const result = await rep.delete({foo: {eq: 10}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 5},
+        ]);
+      });
+
+      it('matches by the "neq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 5});
+        const created3 = await rep.create({foo: 10});
+        const result = await rep.delete({foo: {neq: 10}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created3[DEF_PK]), foo: 10},
+        ]);
+      });
+
+      it('matches by the "gt" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.delete({foo: {gt: 10}});
+        expect(result).to.be.eq(1);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 5},
+          {_id: new ObjectId(created2[DEF_PK]), foo: 10},
+        ]);
+      });
+
+      it('matches by the "lt" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 15});
+        const result = await rep.delete({foo: {lt: 10}});
+        expect(result).to.be.eq(1);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created2[DEF_PK]), foo: 10},
+          {_id: new ObjectId(created3[DEF_PK]), foo: 15},
+        ]);
+      });
+
+      it('matches by the "gte" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.delete({foo: {gte: 10}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 5},
+        ]);
+      });
+
+      it('matches by the "lte" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 15});
+        const result = await rep.delete({foo: {lte: 10}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created3[DEF_PK]), foo: 15},
+        ]);
+      });
+
+      it('matches by the "inq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        const created3 = await rep.create({foo: 15});
+        const result = await rep.delete({foo: {inq: [5, 10]}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created3[DEF_PK]), foo: 15},
+        ]);
+      });
+
+      it('matches by the "nin" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        const created2 = await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.delete({foo: {nin: [5, 10]}});
+        expect(result).to.be.eq(1);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 5},
+          {_id: new ObjectId(created2[DEF_PK]), foo: 10},
+        ]);
+      });
+
+      it('matches by the "between" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const created4 = await rep.create({foo: 20});
+        const result = await rep.delete({foo: {between: [9, 16]}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 5},
+          {_id: new ObjectId(created4[DEF_PK]), foo: 20},
+        ]);
+      });
+
+      it('matches by the "exists" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({});
+        await rep.create({foo: undefined});
+        await rep.create({foo: null});
+        await rep.create({foo: 10});
+        const result = await rep.delete({foo: {exists: true}});
+        expect(result).to.be.eq(3);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([{_id: new ObjectId(created1[DEF_PK])}]);
+      });
+
+      it('matches by the "like" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        const created3 = await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.delete({foo: {like: 'sit amet'}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 'lorem ipsum'},
+          {_id: new ObjectId(created3[DEF_PK]), foo: 'DOLOR SIT AMET'},
+        ]);
+      });
+
+      it('matches by the "nlike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        const created2 = await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        const created4 = await rep.create({foo: 'sit amet'});
+        const result = await rep.delete({foo: {nlike: 'sit amet'}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created2[DEF_PK]), foo: 'dolor sit amet'},
+          {_id: new ObjectId(created4[DEF_PK]), foo: 'sit amet'},
+        ]);
+      });
+
+      it('matches by the "ilike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.delete({foo: {ilike: 'sit amet'}});
+        expect(result).to.be.eq(3);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 'lorem ipsum'},
+        ]);
+      });
+
+      it('matches by the "nilike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        const created2 = await rep.create({foo: 'dolor sit amet'});
+        const created3 = await rep.create({foo: 'DOLOR SIT AMET'});
+        const created4 = await rep.create({foo: 'sit amet'});
+        const result = await rep.delete({foo: {nilike: 'sit amet'}});
+        expect(result).to.be.eq(1);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created2[DEF_PK]), foo: 'dolor sit amet'},
+          {_id: new ObjectId(created3[DEF_PK]), foo: 'DOLOR SIT AMET'},
+          {_id: new ObjectId(created4[DEF_PK]), foo: 'sit amet'},
+        ]);
+      });
+
+      it('matches by the "regexp" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        const created3 = await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.delete({foo: {regexp: 'sit am+'}});
+        expect(result).to.be.eq(2);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 'lorem ipsum'},
+          {_id: new ObjectId(created3[DEF_PK]), foo: 'DOLOR SIT AMET'},
+        ]);
+      });
+
+      it('matches by the "regexp" operator with flags', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        const created1 = await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.delete({foo: {regexp: 'sit am+', flags: 'i'}});
+        expect(result).to.be.eq(3);
+        const rawData = await MDB_CLIENT.db()
+          .collection('model')
+          .find()
+          .toArray();
+        expect(rawData).to.be.eql([
+          {_id: new ObjectId(created1[DEF_PK]), foo: 'lorem ipsum'},
+        ]);
+      });
+    });
+  });
+
+  describe('deleteById', function () {
+    it('returns false if a given identifier is not exist', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const result = await rep.deleteById(oid);
+      expect(result).to.be.false;
+    });
+
+    it('returns true if an item has removed by a given identifier', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      await rep.create({[DEF_PK]: oid});
+      const result = await rep.deleteById(oid);
+      expect(result).to.be.true;
+      const rawData = await MDB_CLIENT.db()
+        .collection('model')
+        .find()
+        .toArray();
+      expect(rawData).to.be.empty;
+    });
+  });
+
+  describe('exists', function () {
+    it('returns false if a given identifier is not exist', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      const result = await rep.exists(oid);
+      expect(result).to.be.false;
+    });
+
+    it('returns true if a given identifier is exist', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const oid = new ObjectId();
+      await rep.create({[DEF_PK]: oid});
+      const result = await rep.exists(oid);
+      expect(result).to.be.true;
+    });
+  });
+
+  describe('count', function () {
+    it('returns zero if nothing to count', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      const result = await rep.count();
+      expect(result).to.be.eq(0);
+    });
+
+    it('returns a number of table items', async function () {
+      const schema = createSchema();
+      schema.defineModel({name: 'model', datasource: 'mongodb'});
+      const rep = schema.getRepository('model');
+      await rep.create({});
+      await rep.create({});
+      await rep.create({});
+      const result = await rep.count();
+      expect(result).to.be.eq(3);
+    });
+
+    describe('uses a where clause to count items', function () {
+      it('matches by a document subset', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'a'});
+        await rep.create({foo: 'b'});
+        await rep.create({foo: 'b'});
+        const result = await rep.count({foo: 'b'});
+        expect(result).to.be.eql(2);
+      });
+
+      it('matches by the "eq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 10});
+        const result = await rep.count({foo: {eq: 10}});
+        expect(result).to.be.eql(2);
+      });
+
+      it('matches by the "neq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 10});
+        const result = await rep.count({foo: {neq: 10}});
+        expect(result).to.be.eq(1);
+      });
+
+      it('matches by the "gt" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.count({foo: {gt: 10}});
+        expect(result).to.be.eq(1);
+      });
+
+      it('matches by the "lt" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.count({foo: {lt: 10}});
+        expect(result).to.be.eq(1);
+      });
+
+      it('matches by the "gte" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.count({foo: {gte: 10}});
+        expect(result).to.be.eq(2);
+      });
+
+      it('matches by the "lte" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.count({foo: {lte: 10}});
+        expect(result).to.be.eq(2);
+      });
+
+      it('matches by the "inq" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.count({foo: {inq: [5, 10]}});
+        expect(result).to.be.eq(2);
+      });
+
+      it('matches by the "nin" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        const result = await rep.count({foo: {nin: [5, 10]}});
+        expect(result).to.be.eq(1);
+      });
+
+      it('matches by the "between" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 5});
+        await rep.create({foo: 10});
+        await rep.create({foo: 15});
+        await rep.create({foo: 20});
+        const result = await rep.count({foo: {between: [9, 16]}});
+        expect(result).to.be.eq(2);
+      });
+
+      it('matches by the "exists" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({});
+        await rep.create({foo: undefined});
+        await rep.create({foo: null});
+        await rep.create({foo: 10});
+        const result1 = await rep.count({foo: {exists: true}});
+        expect(result1).to.be.eq(3);
+      });
+
+      it('matches by the "like" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.count({foo: {like: 'sit amet'}});
+        expect(result).to.be.eql(2);
+      });
+
+      it('matches by the "nlike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.count({foo: {nlike: 'sit amet'}});
+        expect(result).to.be.eql(2);
+      });
+
+      it('matches by the "ilike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.count({foo: {ilike: 'sit amet'}});
+        expect(result).to.be.eql(3);
+      });
+
+      it('matches by the "nilike" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.count({foo: {nilike: 'sit amet'}});
+        expect(result).to.be.eql(1);
+      });
+
+      it('matches by the "regexp" operator', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.count({foo: {regexp: 'sit am+'}});
+        expect(result).to.be.eql(2);
+      });
+
+      it('matches by the "regexp" operator with flags', async function () {
+        const schema = createSchema();
+        schema.defineModel({name: 'model', datasource: 'mongodb'});
+        const rep = schema.getRepository('model');
+        await rep.create({foo: 'lorem ipsum'});
+        await rep.create({foo: 'dolor sit amet'});
+        await rep.create({foo: 'DOLOR SIT AMET'});
+        await rep.create({foo: 'sit amet'});
+        const result = await rep.count({foo: {regexp: 'sit am+', flags: 'i'}});
+        expect(result).to.be.eql(3);
+      });
+    });
+  });
+});

+ 87 - 0
src/utils/create-mongodb-url.js

@@ -0,0 +1,87 @@
+import {InvalidArgumentError} from '@e22m4u/repository';
+
+/**
+ * Generate the mongodb URL from the options.
+ */
+export function createMongodbUrl(options = {}) {
+  if (!options || typeof options !== 'object' || Array.isArray(options))
+    throw new InvalidArgumentError(
+      'The first argument of "createMongodbUrl" must be an Object, but %s given.',
+      options,
+    );
+  if (options.protocol && typeof options.protocol !== 'string')
+    throw new InvalidArgumentError(
+      'MongoDB option "protocol" must be a string, but %s given.',
+      options.protocol,
+    );
+  if (options.hostname && typeof options.hostname !== 'string')
+    throw new InvalidArgumentError(
+      'MongoDB option "hostname" must be a String, but %s given.',
+      options.hostname,
+    );
+  if (options.host && typeof options.host !== 'string')
+    throw new InvalidArgumentError(
+      'MongoDB option "host" must be a String, but %s given.',
+      options.host,
+    );
+  if (
+    options.port &&
+    typeof options.port !== 'number' &&
+    typeof options.port !== 'string'
+  ) {
+    throw new InvalidArgumentError(
+      'MongoDB option "port" must be a Number or a String, but %s given.',
+      options.port,
+    );
+  }
+  if (options.database && typeof options.database !== 'string')
+    throw new InvalidArgumentError(
+      'MongoDB option "database" must be a String, but %s given.',
+      options.database,
+    );
+  if (options.db && typeof options.db !== 'string')
+    throw new InvalidArgumentError(
+      'MongoDB option "db" must be a String, but %s given.',
+      options.db,
+    );
+  if (options.username && typeof options.username !== 'string')
+    throw new InvalidArgumentError(
+      'MongoDB option "username" must be a String, but %s given.',
+      options.username,
+    );
+  if (
+    options.password &&
+    typeof options.password !== 'string' &&
+    typeof options.password !== 'number'
+  ) {
+    throw new InvalidArgumentError(
+      'MongoDB option "password" must be a String or a Number, but %s given.',
+      options.password,
+    );
+  }
+  if (
+    options.pass &&
+    typeof options.pass !== 'string' &&
+    typeof options.pass !== 'number'
+  ) {
+    throw new InvalidArgumentError(
+      'MongoDB option "pass" must be a String or a Number, but %s given.',
+      options.pass,
+    );
+  }
+  const protocol = options.protocol || 'mongodb';
+  const hostname = options.hostname || options.host || '127.0.0.1';
+  const port = options.port || 27017;
+  const database = options.database || options.db || 'database';
+  const username = options.username || options.user;
+  const password = options.password || options.pass || undefined;
+  let portUrl = '';
+  if (protocol !== 'mongodb+srv') {
+    portUrl = ':' + port;
+  }
+  if (username && password) {
+    return `${protocol}://${username}:${password}@${hostname}${portUrl}/${database}`;
+  } else {
+    return `${protocol}://${hostname}${portUrl}/${database}`;
+  }
+}

+ 29 - 0
src/utils/create-mongodb-url.spec.js

@@ -0,0 +1,29 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {createMongodbUrl} from './create-mongodb-url.js';
+
+describe('createMongodbUrl', function () {
+  it('returns a string representation of default values', function () {
+    const value = createMongodbUrl();
+    expect(value).to.be.eq('mongodb://127.0.0.1:27017/database');
+  });
+
+  it('throws an error when the first argument is a non-object value', function () {
+    const throwable = v => () => createMongodbUrl(v);
+    const error = v =>
+      format(
+        'The first argument of "createMongodbUrl" must be an Object, but %s given.',
+        v,
+      );
+    expect(throwable('')).to.throw(error('""'));
+    expect(throwable('str')).to.throw(error('"str"'));
+    expect(throwable(0)).to.throw(error('0'));
+    expect(throwable(10)).to.throw(error('10'));
+    expect(throwable(true)).to.throw(error('true'));
+    expect(throwable(false)).to.throw(error('false'));
+    expect(throwable([])).to.throw(error('Array'));
+    expect(throwable(null)).to.throw(error('null'));
+    throwable(undefined)();
+    throwable({})();
+  });
+});

+ 3 - 0
src/utils/index.js

@@ -0,0 +1,3 @@
+export * from './is-object-id.js';
+export * from './create-mongodb-url.js';
+export * from './transform-values-deep.js';

+ 13 - 0
src/utils/is-iso-date.js

@@ -0,0 +1,13 @@
+/**
+ * Is iso date string.
+ *
+ * @param value
+ * @return {boolean}
+ */
+export function isIsoDate(value) {
+  if (!value) return false;
+  if (value instanceof Date) return true;
+  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(value)) return false;
+  const d = new Date(value);
+  return d instanceof Date && !isNaN(d.getTime()) && d.toISOString() === value;
+}

+ 14 - 0
src/utils/is-object-id.js

@@ -0,0 +1,14 @@
+import {ObjectId} from 'mongodb';
+
+/**
+ * Is object id.
+ *
+ * @param value
+ * @return {boolean}
+ */
+export function isObjectId(value) {
+  if (!value) return false;
+  if (value instanceof ObjectId) return true;
+  if (typeof value !== 'string') return false;
+  return value.match(/^[a-fA-F0-9]{24}$/) != null;
+}

+ 24 - 0
src/utils/is-object-id.spec.js

@@ -0,0 +1,24 @@
+import {expect} from 'chai';
+import {isObjectId} from './is-object-id.js';
+import {ObjectId} from 'mongodb';
+
+describe('isObjectId', function () {
+  it('returns true for a valid ObjectId string or an instance', function () {
+    expect(isObjectId('')).to.be.false;
+    expect(isObjectId('123')).to.be.false;
+    expect(isObjectId(0)).to.be.false;
+    expect(isObjectId(10)).to.be.false;
+    expect(isObjectId(true)).to.be.false;
+    expect(isObjectId(false)).to.be.false;
+    expect(isObjectId({})).to.be.false;
+    expect(isObjectId({foo: 'bar'})).to.be.false;
+    expect(isObjectId([])).to.be.false;
+    expect(isObjectId(['foo'])).to.be.false;
+    expect(isObjectId(new Date())).to.be.false;
+    expect(isObjectId(null)).to.be.false;
+    expect(isObjectId(undefined)).to.be.false;
+    //
+    expect(isObjectId(new ObjectId())).to.be.true;
+    expect(isObjectId(String(new ObjectId()))).to.be.true;
+  });
+});

+ 38 - 0
src/utils/transform-values-deep.js

@@ -0,0 +1,38 @@
+import {InvalidArgumentError} from '@e22m4u/repository';
+
+/**
+ * Transform values deep.
+ *
+ * @param value
+ * @param transformer
+ * @return {*}
+ */
+export function transformValuesDeep(value, transformer) {
+  if (!transformer || typeof transformer !== 'function')
+    throw InvalidArgumentError(
+      'The second argument of "transformValuesDeep" ' +
+        'must be a Function, but %s given.',
+      transformer,
+    );
+  if (Array.isArray(value)) {
+    value.forEach((v, i) => (value[i] = transformValuesDeep(v, transformer)));
+    return value;
+  } else if (value && typeof value === 'object') {
+    // pure object
+    if (
+      !value.constructor ||
+      (value.constructor && value.constructor.name === 'Object')
+    ) {
+      Object.keys(value).forEach(key => {
+        if (Object.prototype.hasOwnProperty.call(value, key))
+          value[key] = transformValuesDeep(value[key], transformer);
+      });
+      return value;
+      // Date, ObjectId etc..
+    } else {
+      return transformer(value);
+    }
+  } else {
+    return transformer(value);
+  }
+}

+ 47 - 0
src/utils/transform-values-deep.spec.js

@@ -0,0 +1,47 @@
+import {expect} from 'chai';
+import {transformValuesDeep} from './transform-values-deep.js';
+
+describe('transformValuesDeep', function () {
+  it('transforms property values of an object', function () {
+    const object = {
+      foo: 1,
+      bar: {
+        baz: 2,
+        qux: {
+          qwe: 3,
+        },
+      },
+    };
+    const result = transformValuesDeep(object, String);
+    expect(result).to.be.eql({
+      foo: '1',
+      bar: {
+        baz: '2',
+        qux: {
+          qwe: '3',
+        },
+      },
+    });
+  });
+
+  it('transforms elements of an array', function () {
+    const object = [1, 2, 3, [4, 5, 6, [7, 8, 9]]];
+    const result = transformValuesDeep(object, String);
+    expect(result).to.be.eql(['1', '2', '3', ['4', '5', '6', ['7', '8', '9']]]);
+  });
+
+  it('transforms non-pure objects', function () {
+    const date = new Date();
+    const str = String(date);
+    const result1 = transformValuesDeep(date, String);
+    const result2 = transformValuesDeep([date], String);
+    const result3 = transformValuesDeep([[date]], String);
+    const result4 = transformValuesDeep({date}, String);
+    const result5 = transformValuesDeep({foo: {date}}, String);
+    expect(result1).to.be.eql(str);
+    expect(result2).to.be.eql([str]);
+    expect(result3).to.be.eql([[str]]);
+    expect(result4).to.be.eql({date: str});
+    expect(result5).to.be.eql({foo: {date: str}});
+  });
+});

+ 4 - 0
test.env

@@ -0,0 +1,4 @@
+MONGODB_CONTAINER=mongodb_c
+MONGODB_HOST=localhost
+MONGODB_PORT=27017
+MONGODB_DATABASE=test