|
|
@@ -0,0 +1,2696 @@
|
|
|
+## @e22m4u/js-repository
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+[Русский](./README.md) | English
|
|
|
+
|
|
|
+<br/>
|
|
|
+<div align="center">
|
|
|
+ <img alt="logo" src="assets/logo.png"/>
|
|
|
+</div>
|
|
|
+<br/>
|
|
|
+<br/>
|
|
|
+
|
|
|
+An implementation of the Repository pattern for working with databases in
|
|
|
+Node.js.
|
|
|
+
|
|
|
+- [Installation](#installation)
|
|
|
+- [Importing](#importing)
|
|
|
+- [Description](#description)
|
|
|
+- [Example](#example)
|
|
|
+- [Schema](#schema)
|
|
|
+- [Datasource](#datasource)
|
|
|
+- [Model](#model)
|
|
|
+- [Properties](#properties)
|
|
|
+- [Validators](#validators)
|
|
|
+ - [Global Validators](#global-validators)
|
|
|
+ - [Registering Global Validators](#registering-global-validators)
|
|
|
+ - [Local Validators](#local-validators)
|
|
|
+- [Transformers](#transformers)
|
|
|
+ - [Global Transformers](#global-transformers)
|
|
|
+ - [Registering Global Transformers](#registering-global-transformers)
|
|
|
+ - [Local Transformers](#local-transformers)
|
|
|
+- [Empty Values](#empty-values)
|
|
|
+ - [Overriding Empty Values](#overriding-empty-values)
|
|
|
+- [Repository](#repository)
|
|
|
+ - [create](#repositorycreate)
|
|
|
+ - [replaceById](#repositoryreplacebyid)
|
|
|
+ - [replaceOrCreate](#repositoryreplaceorcreate)
|
|
|
+ - [patchById](#repositorypatchbyid)
|
|
|
+ - [patch](#repositorypatch)
|
|
|
+ - [find](#repositoryfind)
|
|
|
+ - [findOne](#repositoryfindone)
|
|
|
+ - [findById](#repositoryfindbyid)
|
|
|
+ - [delete](#repositorydelete)
|
|
|
+ - [deleteById](#repositorydeletebyid)
|
|
|
+ - [exists](#repositoryexists)
|
|
|
+ - [count](#repositorycount)
|
|
|
+- [Filtering](#filtering)
|
|
|
+- [Relations](#relations)
|
|
|
+ - [Belongs To](#belongs-to)
|
|
|
+ - [Has One](#has-one)
|
|
|
+ - [Has Many](#has-many)
|
|
|
+ - [References Many](#references-many)
|
|
|
+ - [Belongs To (Polymorphic)](#belongs-to-polymorphic-version)
|
|
|
+ - [Has One (Polymorphic)](#has-one-polymorphic-version)
|
|
|
+ - [Has Many (Polymorphic)](#has-many-polymorphic-version)
|
|
|
+- [Extending](#extending)
|
|
|
+- [TypeScript](#typescript)
|
|
|
+- [Tests](#tests)
|
|
|
+- [License](#license)
|
|
|
+
|
|
|
+## Installation
|
|
|
+
|
|
|
+```bash
|
|
|
+npm install @e22m4u/js-repository
|
|
|
+```
|
|
|
+
|
|
|
+Optionally, install the required adapter.
|
|
|
+
|
|
|
+| adapter | description |
|
|
|
+|-----------|---------------------------------------------------------------------------------------------------------------------------------|
|
|
|
+| `memory` | An in-process memory database (no installation required) |
|
|
|
+| `mongodb` | MongoDB - a NoSQL database management system (*[install](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter)*) |
|
|
|
+
|
|
|
+## Importing
|
|
|
+
|
|
|
+The module supports both ESM and CommonJS standards.
|
|
|
+
|
|
|
+*ESM*
|
|
|
+
|
|
|
+```js
|
|
|
+import {DatabaseSchema} from '@e22m4u/js-repository';
|
|
|
+```
|
|
|
+
|
|
|
+*CommonJS*
|
|
|
+
|
|
|
+```js
|
|
|
+const {DatabaseSchema} = require('@e22m4u/js-repository');
|
|
|
+```
|
|
|
+
|
|
|
+## Description
|
|
|
+
|
|
|
+The module allows you to abstract away from various database interfaces
|
|
|
+by presenting them as named *datasources* connected to *models*.
|
|
|
+A *Model* describes a database table (or collection), whose columns are the
|
|
|
+model's properties. Model properties can have a specific *type* of allowed
|
|
|
+value, a set of *validators*, and *transformers* that data passes through
|
|
|
+before being written to the database. Additionally, a *model* can define
|
|
|
+classic "one-to-one," "one-to-many," and other types of relationships
|
|
|
+between models.
|
|
|
+
|
|
|
+Data is read and written directly through a *repository*, which is available
|
|
|
+for every model with a declared *datasource*. The repository can filter
|
|
|
+queried documents, validate properties according to the model definition,
|
|
|
+and embed related data into the query results.
|
|
|
+
|
|
|
+- *Datasource* - defines how to connect to the database.
|
|
|
+- *Model* - describes the document structure and relations to other models.
|
|
|
+- *Repository* - performs read and write operations for model documents.
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+
|
|
|
+ A[Schema]
|
|
|
+ subgraph Databases
|
|
|
+ B[Datasource 1]
|
|
|
+ C[Datasource 2]
|
|
|
+ end
|
|
|
+ A-->B
|
|
|
+ A-->C
|
|
|
+
|
|
|
+ subgraph Collections
|
|
|
+ D[Model A]
|
|
|
+ E[Model B]
|
|
|
+ F[Model C]
|
|
|
+ G[Model D]
|
|
|
+ end
|
|
|
+ B-->D
|
|
|
+ B-->E
|
|
|
+ C-->F
|
|
|
+ C-->G
|
|
|
+
|
|
|
+ H[Repository A]
|
|
|
+ I[Repository B]
|
|
|
+ J[Repository C]
|
|
|
+ K[Repository D]
|
|
|
+ D-->H
|
|
|
+ E-->I
|
|
|
+ F-->J
|
|
|
+ G-->K
|
|
|
+```
|
|
|
+
|
|
|
+## Example
|
|
|
+
|
|
|
+This example demonstrates creating a schema instance, declaring a datasource,
|
|
|
+and a `country` model. Then, using the model's repository, a new document
|
|
|
+(a country) is added to the collection and logged to the console.
|
|
|
+
|
|
|
+```
|
|
|
+ Country (country)
|
|
|
+┌─────────────────────────┐
|
|
|
+│ id: 1 │
|
|
|
+│ name: "Russia" │
|
|
|
+│ population: 143400000 │
|
|
|
+└─────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+```js
|
|
|
+import {DataType} from '@e22m4u/js-repository';
|
|
|
+import {DatabaseSchema} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+// create a DatabaseSchema instance
|
|
|
+const dbs = new DatabaseSchema();
|
|
|
+
|
|
|
+// declare the "myDb" datasource
|
|
|
+dbs.defineDatasource({
|
|
|
+ name: 'myDb', // name of the new datasource
|
|
|
+ adapter: 'memory', // chosen adapter
|
|
|
+});
|
|
|
+
|
|
|
+// declare the "country" model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'country', // name of the new model
|
|
|
+ datasource: 'myDb', // chosen datasource
|
|
|
+ properties: { // model properties
|
|
|
+ name: DataType.STRING, // type "string"
|
|
|
+ population: DataType.NUMBER, // type "number"
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
+// get the model's repository
|
|
|
+const countryRep = dbs.getRepository('country');
|
|
|
+
|
|
|
+// add a new document to the collection
|
|
|
+const country = await countryRep.create({
|
|
|
+ name: 'Russia',
|
|
|
+ population: 143400000,
|
|
|
+});
|
|
|
+
|
|
|
+// log the new document
|
|
|
+console.log(country);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Russia',
|
|
|
+// population: 143400000,
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+The next block defines a `city` model with a `belongsTo` relation to the
|
|
|
+`country` model from the example above. Then, a new city document is created,
|
|
|
+linked to the previously created country. After creating the new document, a
|
|
|
+query is executed to retrieve this city with its related country included.
|
|
|
+
|
|
|
+```
|
|
|
+ Country (country) City (city)
|
|
|
+┌─────────────────────────┐ ┌─────────────────────────┐
|
|
|
+│ id: 1 <───────────────│───┐ │ id: 1 │
|
|
|
+│ name: "Russia" │ │ │ name: "Moscow" │
|
|
|
+│ population: 143400000 │ └───│─ countryId: 1 │
|
|
|
+└─────────────────────────┘ └─────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+```js
|
|
|
+// declare the "city" model with a relation to "country"
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'city',
|
|
|
+ datasource: 'myDb',
|
|
|
+ properties: {
|
|
|
+ name: DataType.STRING,
|
|
|
+ countryId: DataType.NUMBER,
|
|
|
+ // defining the "countryId" foreign key is not strictly necessary,
|
|
|
+ // but it is recommended for type validation before writing to the
|
|
|
+ // database, as the "memory" adapter creates numeric IDs by default
|
|
|
+ },
|
|
|
+ relations: {
|
|
|
+ // defining the "country" relation allows for automatic inclusion
|
|
|
+ // of related documents using the "include" option in repository methods
|
|
|
+ country: {
|
|
|
+ type: RelationType.BELONGS_TO, // relation type: belongs to...
|
|
|
+ model: 'country', // target model name
|
|
|
+ foreignKey: 'countryId', // foreign key field (optional)
|
|
|
+ // if the foreign key follows the `relationName` + `Id` convention,
|
|
|
+ // the `foreignKey` option is not required.
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// get the repository for the "city" model
|
|
|
+const cityRep = dbs.getRepository('city');
|
|
|
+
|
|
|
+// create a new city and link it to the country via country.id
|
|
|
+const city = await cityRep.create({
|
|
|
+ name: 'Moscow',
|
|
|
+ countryId: country.id, // use the id from the previously created country
|
|
|
+});
|
|
|
+
|
|
|
+console.log(city);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Moscow',
|
|
|
+// countryId: 1,
|
|
|
+// }
|
|
|
+
|
|
|
+// retrieve the city by its ID, including the related country
|
|
|
+const cityWithCountry = await cityRep.findById(city.id, {
|
|
|
+ include: 'country',
|
|
|
+});
|
|
|
+
|
|
|
+console.log(cityWithCountry);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Moscow',
|
|
|
+// countryId: 1,
|
|
|
+// country: {
|
|
|
+// id: 1,
|
|
|
+// name: 'Russia',
|
|
|
+// population: 143400000
|
|
|
+// }
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+## Schema
|
|
|
+
|
|
|
+An instance of the `DatabaseSchema` class stores datasource and model
|
|
|
+definitions.
|
|
|
+
|
|
|
+**Methods**
|
|
|
+
|
|
|
+- `defineDatasource(datasourceDef: object): this` - add a datasource
|
|
|
+- `defineModel(modelDef: object): this` - add a model
|
|
|
+- `getRepository(modelName: string): Repository` - get a repository
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Import the class and create a schema instance.
|
|
|
+
|
|
|
+```js
|
|
|
+import {DatabaseSchema} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+const dbs = new DatabaseSchema();
|
|
|
+```
|
|
|
+
|
|
|
+Define a new datasource.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineDatasource({
|
|
|
+ name: 'myDb', // name of the new datasource
|
|
|
+ adapter: 'memory', // chosen adapter
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Define a new model.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'product', // name of the new model
|
|
|
+ datasource: 'myDb',// chosen datasource
|
|
|
+ properties: { // model properties
|
|
|
+ name: DataType.STRING,
|
|
|
+ weight: DataType.NUMBER,
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Get a repository by the model name.
|
|
|
+
|
|
|
+```js
|
|
|
+const productRep = dbs.getRepository('product');
|
|
|
+```
|
|
|
+
|
|
|
+## Datasource
|
|
|
+
|
|
|
+A datasource stores the name of the selected adapter and its settings.
|
|
|
+A new datasource is defined using the `defineDatasource` method of a
|
|
|
+`DatabaseSchema` instance.
|
|
|
+
|
|
|
+**Parameters**
|
|
|
+
|
|
|
+- `name: string` a unique name
|
|
|
+- `adapter: string` the chosen adapter
|
|
|
+- adapter-specific parameters (if any)
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Define a new datasource.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineDatasource({
|
|
|
+ name: 'myDb', // name of the new datasource
|
|
|
+ adapter: 'memory', // chosen adapter
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Passing additional parameters, using the MongoDB adapter as an example
|
|
|
+(*[install](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter)*).
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineDatasource({
|
|
|
+ name: 'myDb',
|
|
|
+ adapter: 'mongodb',
|
|
|
+ // parameters for the "mongodb" adapter
|
|
|
+ host: '127.0.0.1',
|
|
|
+ port: 27017,
|
|
|
+ database: 'myDatabase',
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Model
|
|
|
+
|
|
|
+Describes the structure of a collection's documents and its relations to
|
|
|
+other models. A new model is defined using the `defineModel` method of a
|
|
|
+`DatabaseSchema` instance.
|
|
|
+
|
|
|
+**Parameters**
|
|
|
+
|
|
|
+- `name: string` model name (required)
|
|
|
+- `base: string` name of the base model to inherit from
|
|
|
+- `tableName: string` name of the collection in the database
|
|
|
+- `datasource: string` the chosen datasource
|
|
|
+- `properties: object` property definitions (see [Properties](#properties))
|
|
|
+- `relations: object` relation definitions (see [Relations](#relations))
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Define a model with properties of specified types.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user', // name of the new model
|
|
|
+ properties: { // model properties
|
|
|
+ name: DataType.STRING,
|
|
|
+ age: DataType.NUMBER,
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Properties
|
|
|
+
|
|
|
+The `properties` parameter in a model definition accepts an object where keys
|
|
|
+are property names and values are either the property type or an object with
|
|
|
+additional parameters.
|
|
|
+
|
|
|
+**Data Types**
|
|
|
+
|
|
|
+- `DataType.ANY` any value is allowed
|
|
|
+- `DataType.STRING` only `string` values
|
|
|
+- `DataType.NUMBER` only `number` values
|
|
|
+- `DataType.BOOLEAN` only `boolean` values
|
|
|
+- `DataType.ARRAY` only `array` values
|
|
|
+- `DataType.OBJECT` only `object` values
|
|
|
+
|
|
|
+**Parameters**
|
|
|
+
|
|
|
+- `type: string` the allowed value type (required)
|
|
|
+- `itemType: string` array item type (for `type: 'array'`)
|
|
|
+- `model: string` object model (for `type: 'object'`)
|
|
|
+- `primaryKey: boolean` declares the property as a primary key
|
|
|
+- `columnName: string` overrides the column name
|
|
|
+- `columnType: string` column type (defined by the adapter)
|
|
|
+- `required: boolean` declares the property as required
|
|
|
+- `default: any` default value
|
|
|
+- `validate: string | Function | array | object` see [Validators](#validators)
|
|
|
+- `unique: boolean | string` check value for uniqueness
|
|
|
+
|
|
|
+**The `unique` Parameter**
|
|
|
+
|
|
|
+If the `unique` parameter is `true` or `'strict'`, a strict uniqueness
|
|
|
+check is performed. In this mode, [empty values](#empty-values) are also
|
|
|
+checked, where `null` and `undefined` are considered values that must be
|
|
|
+unique.
|
|
|
+
|
|
|
+The `'sparse'` mode only checks values with a payload, excluding
|
|
|
+[empty values](#empty-values), the list of which varies depending on the
|
|
|
+property type. For example, for a `string` type, empty values are
|
|
|
+`undefined`, `null`, and `''` (an empty string).
|
|
|
+
|
|
|
+- `unique: true | 'strict'` strict uniqueness check
|
|
|
+- `unique: 'sparse'` exclude [empty values](#empty-values) from the check
|
|
|
+- `unique: false | 'nonUnique'` do not check for uniqueness (default)
|
|
|
+
|
|
|
+You can use predefined constants as equivalents for the string values
|
|
|
+`strict`, `sparse`, and `nonUnique`.
|
|
|
+
|
|
|
+- `PropertyUniqueness.STRICT`
|
|
|
+- `PropertyUniqueness.SPARSE`
|
|
|
+- `PropertyUniqueness.NON_UNIQUE`
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Short-form property definition.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'city',
|
|
|
+ properties: { // model properties
|
|
|
+ name: DataType.STRING, // property type "string"
|
|
|
+ population: DataType.NUMBER, // property type "number"
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Full-form property definition.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'city',
|
|
|
+ properties: { // model properties
|
|
|
+ name: {
|
|
|
+ type: DataType.STRING, // property type "string" (required)
|
|
|
+ required: true, // disallows undefined and null values
|
|
|
+ },
|
|
|
+ population: {
|
|
|
+ type: DataType.NUMBER, // property type "number" (required)
|
|
|
+ default: 0, // default value
|
|
|
+ },
|
|
|
+ code: {
|
|
|
+ type: DataType.NUMBER, // property type "number" (required)
|
|
|
+ unique: PropertyUniqueness.STRICT, // check for uniqueness
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Factory default value. The function's return value will be determined at the
|
|
|
+time the document is written.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'article',
|
|
|
+ properties: { // model properties
|
|
|
+ tags: {
|
|
|
+ type: DataType.ARRAY, // property type "array" (required)
|
|
|
+ itemType: DataType.STRING,// item type "string"
|
|
|
+ default: () => [], // factory value
|
|
|
+ },
|
|
|
+ createdAt: {
|
|
|
+ type: DataType.STRING, // property type "string" (required)
|
|
|
+ default: () => new Date().toISOString(), // factory value
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Validators
|
|
|
+
|
|
|
+Validators are used to check a property's value before writing it to the
|
|
|
+database. Validation occurs immediately after the type check specified in the
|
|
|
+model's property definition. [Empty values](#empty-values) bypass validators
|
|
|
+as they have no payload.
|
|
|
+
|
|
|
+### Global Validators
|
|
|
+
|
|
|
+The module comes with a set of global validators:
|
|
|
+
|
|
|
+- `regexp` validates against a regular expression,
|
|
|
+ *parameter: `string | RegExp` - the regular expression;*
|
|
|
+
|
|
|
+- `maxLength` maximum length for a string or array,
|
|
|
+ *parameter: `number` - the maximum length;*
|
|
|
+
|
|
|
+- `minLength` minimum length for a string or array,
|
|
|
+ *parameter: `number` - the minimum length;*
|
|
|
+
|
|
|
+The validators below are under development:
|
|
|
+
|
|
|
+- `isLowerCase` checks for lowercase letters only;
|
|
|
+- `isUpperCase` checks for uppercase letters only;
|
|
|
+- `isEmail` checks for a valid email format;
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Using a global validator.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ email: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ validate: 'isEmail',
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using global validators as an array.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ email: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ validate: [
|
|
|
+ 'isEmail',
|
|
|
+ 'isLowerCase',
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using global validators with arguments.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ name: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ validate: {
|
|
|
+ minLength: 2,
|
|
|
+ maxLength: 24,
|
|
|
+ regexp: /^[a-zA-Z-']+$/,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Global validators without parameters can accept any arguments.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ email: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ validate: {
|
|
|
+ maxLength: 100,
|
|
|
+ // since the "isEmail" validator has no parameters,
|
|
|
+ // its definition allows any value to be passed
|
|
|
+ // as an argument
|
|
|
+ isEmail: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### Registering Global Validators
|
|
|
+
|
|
|
+A validator is a function that receives the value of the corresponding field
|
|
|
+before it is written to the database. If the function returns `false` during
|
|
|
+validation, a standard error is thrown. You can replace the standard error
|
|
|
+by throwing a custom error directly inside the validation function.
|
|
|
+
|
|
|
+A global validator is registered using the `addValidator` method of the
|
|
|
+`PropertyValidatorRegistry` service, which takes the validator name and the
|
|
|
+validation function.
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Registering a global validator to check UUID format.
|
|
|
+
|
|
|
+```js
|
|
|
+import {createError} from 'http-errors';
|
|
|
+import {format} from '@e22m4u/js-format';
|
|
|
+import {Errorf} from '@e22m4u/js-format';
|
|
|
+import {PropertyValidatorRegistry} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+// get the service instance
|
|
|
+const pvr = dbs.getService(PropertyValidatorRegistry);
|
|
|
+
|
|
|
+// regular expressions for different UUID versions
|
|
|
+const uuidRegex = {
|
|
|
+ any: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
|
+ v4: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
|
+};
|
|
|
+
|
|
|
+// register the global validator "isUuid",
|
|
|
+// which accepts a settings object with a "version" property.
|
|
|
+pvr.addValidator('isUuid', (value, options, context) => {
|
|
|
+ // value - the value being validated;
|
|
|
+ // options - validator parameters;
|
|
|
+ // context - information about the property being validated;
|
|
|
+ console.log(options);
|
|
|
+ // {
|
|
|
+ // version: 'v4'
|
|
|
+ // }
|
|
|
+ console.log(context);
|
|
|
+ // {
|
|
|
+ // validatorName: 'isUuid',
|
|
|
+ // modelName: 'device',
|
|
|
+ // propName: 'deviceId'
|
|
|
+ // }
|
|
|
+
|
|
|
+ // empty values are not passed to validators
|
|
|
+ // (the condition below will never trigger)
|
|
|
+ if (typeof value !== 'string') return false;
|
|
|
+ // find the regex for the specified UUID version
|
|
|
+ // (from the validator's options)
|
|
|
+ const version = options?.version || 'any';
|
|
|
+ const regex = uuidRegex[version];
|
|
|
+ // if the regex is not found,
|
|
|
+ // an internal error is thrown
|
|
|
+ if (!regex)
|
|
|
+ throw new Errorf(
|
|
|
+ 'Invalid UUID version %v specified for validator.',
|
|
|
+ version,
|
|
|
+ );
|
|
|
+ // if validation fails, a 400 BadRequest
|
|
|
+ // error is thrown
|
|
|
+ if (!regex.test(value)) {
|
|
|
+ const versionString = version !== 'any' ? ` (version ${version})` : '';
|
|
|
+ throw createError(400, format(
|
|
|
+ 'The property %v of the model %v must be a valid UUID%s.',
|
|
|
+ context.propName,
|
|
|
+ context.modelName,
|
|
|
+ versionString,
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ // on successful validation, return true;
|
|
|
+ // otherwise, a standard validation error
|
|
|
+ // will be thrown
|
|
|
+ return true;
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using the global validator in a property definition.
|
|
|
+
|
|
|
+```js
|
|
|
+// define the "device" model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'device',
|
|
|
+ properties: {
|
|
|
+ deviceId: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ required: true,
|
|
|
+ validate: {
|
|
|
+ // the value {version: 'v4'} will be passed as the second
|
|
|
+ // argument to the validator function
|
|
|
+ isUuid: {version: 'v4'},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### Local Validators
|
|
|
+
|
|
|
+A validator function can be passed directly in the property definition without
|
|
|
+prior registration. To do this, simply pass the function to the `validate`
|
|
|
+parameter as a value or as an array element alongside other validators.
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Using a local validator to check password strength.
|
|
|
+
|
|
|
+```js
|
|
|
+// the `passwordStrength` validator checks password complexity
|
|
|
+function passwordStrength(value, options, context) {
|
|
|
+ // value - the value being validated;
|
|
|
+ // options - not used;
|
|
|
+ // context - information about the property;
|
|
|
+ console.log(context);
|
|
|
+ // {
|
|
|
+ // validatorName: 'passwordStrength',
|
|
|
+ // modelName: 'user',
|
|
|
+ // propName: 'password'
|
|
|
+ // }
|
|
|
+ const errors = [];
|
|
|
+ if (value.length < 8)
|
|
|
+ errors.push('must be at least 8 characters long');
|
|
|
+ if (!/\d/.test(value))
|
|
|
+ errors.push('must contain at least one number');
|
|
|
+ if (!/[a-zA-Z]/.test(value))
|
|
|
+ errors.push('must contain at least one letter');
|
|
|
+ // if any condition is met,
|
|
|
+ // an error is thrown
|
|
|
+ if (errors.length > 0)
|
|
|
+ throw createError(400, format(
|
|
|
+ 'Value of the property %v of the model %v %s.',
|
|
|
+ context.propName,
|
|
|
+ context.modelName,
|
|
|
+ errors.join(', '),
|
|
|
+ ));
|
|
|
+ // on successful validation, return true;
|
|
|
+ // otherwise, a standard validation error
|
|
|
+ // will be thrown
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+// define the "user" model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ password: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ required: true,
|
|
|
+ validate: passwordStrength, // <=
|
|
|
+ // or
|
|
|
+ // validate: [passwordStrength, ...]
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using an anonymous validator function to check a slug.
|
|
|
+
|
|
|
+```js
|
|
|
+// define the "article" model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'article',
|
|
|
+ properties: {
|
|
|
+ slug: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ validate: (value) => {
|
|
|
+ const re = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
+ return re.test(value);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Transformers
|
|
|
+
|
|
|
+Transformers are used to modify a property's value before type checking and
|
|
|
+sending the data to the database. Transformers allow for automatic cleaning
|
|
|
+or formatting of data. [Empty values](#empty-values) are not passed to
|
|
|
+transformers as they have no payload.
|
|
|
+
|
|
|
+### Global Transformers
|
|
|
+
|
|
|
+The module comes with a set of global transformers:
|
|
|
+
|
|
|
+- `trim` removes whitespace from the beginning and end of a string;
|
|
|
+- `toUpperCase` converts a string to uppercase;
|
|
|
+- `toLowerCase` converts a string to lowercase;
|
|
|
+
|
|
|
+The transformers below are under development:
|
|
|
+
|
|
|
+- `cut` truncates a string or array to the specified length,
|
|
|
+ *parameter: `number` - the maximum length;*
|
|
|
+
|
|
|
+- `truncate` truncates a string and appends an ellipsis,
|
|
|
+ *parameter: `number` - the maximum length;*
|
|
|
+
|
|
|
+- `capitalize` converts the first letter of each word to uppercase,
|
|
|
+ *parameter: `{firstWordOnly?: boolean}`;*
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Using a global transformer.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ username: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: 'toLowerCase',
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using global transformers as an array.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ firstName: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: [
|
|
|
+ 'trim',
|
|
|
+ 'capitalize',
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using global transformers with arguments.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'article',
|
|
|
+ properties: {
|
|
|
+ annotation: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: {
|
|
|
+ truncate: 200,
|
|
|
+ capitalize: {firstWordOnly: true},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Global transformers without parameters can accept any arguments.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ firstName: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: {
|
|
|
+ cut: 60,
|
|
|
+ // since the "trim" transformer has no parameters,
|
|
|
+ // its definition allows any value to be passed
|
|
|
+ // as an argument
|
|
|
+ trim: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### Registering Global Transformers
|
|
|
+
|
|
|
+A transformer is a function that takes a property's value and returns a new
|
|
|
+value. The function can be either synchronous or asynchronous (return a
|
|
|
+`Promise`).
|
|
|
+
|
|
|
+A global transformer is registered using the `addTransformer` method of the
|
|
|
+`PropertyTransformerRegistry` service, which takes the transformer name and
|
|
|
+the function itself.
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Registering a global transformer to remove HTML tags.
|
|
|
+
|
|
|
+```js
|
|
|
+import {PropertyTransformerRegistry} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+// get the service instance
|
|
|
+const ptr = dbs.getService(PropertyTransformerRegistry);
|
|
|
+
|
|
|
+// register the global transformer "stripTags"
|
|
|
+ptr.addTransformer('stripTags', (value, options, context) => {
|
|
|
+ // value - the value being transformed;
|
|
|
+ // options - transformer settings (if provided);
|
|
|
+ // context - information about the property;
|
|
|
+ console.log(context);
|
|
|
+ // {
|
|
|
+ // transformerName: 'stripTags',
|
|
|
+ // modelName: 'comment',
|
|
|
+ // propName: 'text'
|
|
|
+ // }
|
|
|
+
|
|
|
+ if (typeof value !== 'string')
|
|
|
+ return value; // return as is if not a string
|
|
|
+
|
|
|
+ return value.replace(/<[^>]*>?/gm, '');
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using the global transformer in a model definition.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'comment',
|
|
|
+ properties: {
|
|
|
+ text: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: 'stripTags',
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### Local Transformers
|
|
|
+
|
|
|
+A transformer function can be passed directly in the property definition
|
|
|
+without prior registration. To do this, simply pass the function to the
|
|
|
+`transform` parameter as a value or as an array element.
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Using a local transformer to normalize names.
|
|
|
+
|
|
|
+```js
|
|
|
+// function to normalize a name
|
|
|
+function normalizeName(value, options, context) {
|
|
|
+ // value - the value being transformed
|
|
|
+ // options - not used
|
|
|
+ // context - information about the property
|
|
|
+ if (!value || typeof value !== 'string') return value;
|
|
|
+ return value
|
|
|
+ .trim() // remove leading/trailing whitespace
|
|
|
+ .toLowerCase() // convert to lowercase
|
|
|
+ .split(' ') // split into words
|
|
|
+ // capitalize the first letter of each word
|
|
|
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
|
+ .join(' '); // join the array back into a string
|
|
|
+}
|
|
|
+
|
|
|
+// define the "user" model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ firstName: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: normalizeName, // <=
|
|
|
+ },
|
|
|
+ lastName: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: normalizeName, // <=
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using a local asynchronous transformer to hash a password.
|
|
|
+
|
|
|
+```js
|
|
|
+import * as bcrypt from 'bcrypt';
|
|
|
+
|
|
|
+// asynchronous function to hash a value
|
|
|
+async function hash(value, options, context) {
|
|
|
+ // value - the value being transformed
|
|
|
+ // options - not used
|
|
|
+ // context - information about the property
|
|
|
+ console.log(context);
|
|
|
+ // {
|
|
|
+ // transformerName: 'hash',
|
|
|
+ // modelName: 'user',
|
|
|
+ // propName: 'password'
|
|
|
+ // }
|
|
|
+ const saltRounds = 10;
|
|
|
+ return bcrypt.hash(value, saltRounds);
|
|
|
+}
|
|
|
+
|
|
|
+// define the "user" model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ properties: {
|
|
|
+ password: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: hash, // <=
|
|
|
+ // or
|
|
|
+ // transform: [hash, ...]
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using an anonymous transformer function to correct a slug.
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'article',
|
|
|
+ properties: {
|
|
|
+ slug: {
|
|
|
+ type: DataType.STRING,
|
|
|
+ transform: (value) => {
|
|
|
+ if (typeof value !== 'string') return value;
|
|
|
+ return value.toLowerCase().replace(/\s+/g, '-');
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Empty Values
|
|
|
+
|
|
|
+Different property types have their own sets of empty values. These sets are
|
|
|
+used to determine if a property's value has a payload. For example, the
|
|
|
+`default` parameter in a property definition sets a default value only if the
|
|
|
+incoming value is empty. The `required` parameter disallows empty values by
|
|
|
+throwing an error. And the `unique` parameter in `sparse` mode allows
|
|
|
+duplicate empty values for a unique property, as they are not included in the
|
|
|
+uniqueness check.
|
|
|
+
|
|
|
+| type | empty values |
|
|
|
+|-------------|---------------------------|
|
|
|
+| `'any'` | `undefined`, `null` |
|
|
|
+| `'string'` | `undefined`, `null`, `''` |
|
|
|
+| `'number'` | `undefined`, `null`, `0` |
|
|
|
+| `'boolean'` | `undefined`, `null` |
|
|
|
+| `'array'` | `undefined`, `null`, `[]` |
|
|
|
+| `'object'` | `undefined`, `null`, `{}` |
|
|
|
+
|
|
|
+### Overriding Empty Values
|
|
|
+
|
|
|
+The set of empty values for any data type can be overridden. These sets are
|
|
|
+managed through a special service provided by the
|
|
|
+[@e22m4u/js-empty-values](https://www.npmjs.com/package/@e22m4u/js-empty-values)
|
|
|
+module (no installation required).
|
|
|
+
|
|
|
+**EmptyValuesService**
|
|
|
+
|
|
|
+To override empty values, you need to get an instance of the
|
|
|
+`EmptyValuesService` class from the schema's container and call the method
|
|
|
+that accepts the data type and an array of new values.
|
|
|
+
|
|
|
+Interface:
|
|
|
+
|
|
|
+```ts
|
|
|
+class EmptyValuesService {
|
|
|
+ /**
|
|
|
+ * Set empty values
|
|
|
+ * for a specific data type.
|
|
|
+ *
|
|
|
+ * @param dataType The data type.
|
|
|
+ * @param emptyValues An array of new empty values.
|
|
|
+ */
|
|
|
+ setEmptyValuesOf(
|
|
|
+ dataType: DataType,
|
|
|
+ emptyValues: unknown[],
|
|
|
+ ): this;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Example**
|
|
|
+
|
|
|
+By default, the value `0` is considered empty for numeric properties. The
|
|
|
+following example shows how to change this behavior, leaving only `undefined`
|
|
|
+and `null` as empty values.
|
|
|
+
|
|
|
+```js
|
|
|
+import {DataType} from '@e22m4u/js-repository';
|
|
|
+import {DatabaseSchema} from '@e22m4u/js-repository';
|
|
|
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
|
|
|
+
|
|
|
+const dbs = new DatabaseSchema();
|
|
|
+
|
|
|
+// get the service for working with empty values
|
|
|
+const emptyValuesService = dbs.getService(EmptyValuesService);
|
|
|
+
|
|
|
+// override empty values for the DataType.NUMBER type
|
|
|
+emptyValuesService.setEmptyValuesOf(DataType.NUMBER, [undefined, null]);
|
|
|
+```
|
|
|
+
|
|
|
+After this, the value `0` for properties of type `DataType.NUMBER` will no
|
|
|
+longer be considered empty, will pass validator checks, and will not be
|
|
|
+replaced by a default value.
|
|
|
+
|
|
|
+## Repository
|
|
|
+
|
|
|
+The repository performs read and write operations for a specific model's data.
|
|
|
+It acts as an intermediary between the application's business logic and the
|
|
|
+database.
|
|
|
+
|
|
|
+**Methods**
|
|
|
+
|
|
|
+- [`create(data, filter = undefined)`](#repositorycreate) creates a new document;
|
|
|
+- [`replaceById(id, data, filter = undefined)`](#repositoryreplacebyid) completely replaces a document;
|
|
|
+- [`replaceOrCreate(data, filter = undefined)`](#repositoryreplaceorcreate) replaces or creates a new document;
|
|
|
+- [`patchById(id, data, filter = undefined)`](#repositorypatchbyid) partially updates a document;
|
|
|
+- [`patch(data, where = undefined)`](#repositorypatch) updates all or matching documents;
|
|
|
+- [`find(filter = undefined)`](#repositoryfind) finds all or matching documents;
|
|
|
+- [`findOne(filter = undefined)`](#repositoryfindone) finds the first matching document;
|
|
|
+- [`findById(id, filter = undefined)`](#repositoryfindbyid) finds a document by its ID;
|
|
|
+- [`delete(where = undefined)`](#repositorydelete) deletes all or matching documents;
|
|
|
+- [`deleteById(id)`](#repositorydeletebyid) deletes a document by its ID;
|
|
|
+- [`exists(id)`](#repositoryexists) checks for existence by ID;
|
|
|
+- [`count(where = undefined)`](#repositorycount) counts all or matching documents;
|
|
|
+
|
|
|
+**Arguments**
|
|
|
+
|
|
|
+- `id: number|string` identifier (primary key)
|
|
|
+- `data: object` document data (used for writing)
|
|
|
+- `where: object` filter conditions (see [Filtering](#filtering))
|
|
|
+- `filter: object` query parameters (see [Filtering](#filtering))
|
|
|
+
|
|
|
+**Getting a Repository**
|
|
|
+
|
|
|
+You can get a repository using the `getRepository()` method of a
|
|
|
+`DatabaseSchema` instance. The method takes the model name as an argument.
|
|
|
+The model must have a defined [datasource](#datasource), as the repository
|
|
|
+interacts directly with the database through the adapter specified in the
|
|
|
+datasource.
|
|
|
+
|
|
|
+```js
|
|
|
+// declare a datasource
|
|
|
+dbs.defineDatasource({
|
|
|
+ name: 'myDatasource',
|
|
|
+ adapter: 'memory', // adapter
|
|
|
+});
|
|
|
+
|
|
|
+// declare a model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'myModel',
|
|
|
+ datasource: 'myDatasource',
|
|
|
+ // properties: { ... },
|
|
|
+ // relations: { ... }
|
|
|
+});
|
|
|
+
|
|
|
+// get the model's repository
|
|
|
+const modelRep = dbs.getRepository('myModel');
|
|
|
+```
|
|
|
+
|
|
|
+The first time `getRepository('myModel')` is called, a new repository instance
|
|
|
+is created and cached. All subsequent calls with the same model name will
|
|
|
+return the existing instance.
|
|
|
+
|
|
|
+### repository.create
|
|
|
+
|
|
|
+Creates a new document in the collection based on the provided data. Returns
|
|
|
+the created document with an assigned identifier.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+create(
|
|
|
+ data: WithOptionalId<FlatData, IdName>,
|
|
|
+ filter?: ItemFilterClause<FlatData>,
|
|
|
+): Promise<FlatData>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Create a new document.
|
|
|
+
|
|
|
+```js
|
|
|
+const newProduct = await productRep.create({
|
|
|
+ name: 'Laptop',
|
|
|
+ price: 1200,
|
|
|
+});
|
|
|
+console.log(newProduct);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Laptop',
|
|
|
+// price: 1200,
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+Create a document, returning only specific fields.
|
|
|
+
|
|
|
+```js
|
|
|
+const product = await productRep.create(
|
|
|
+ {name: 'Mouse', price: 25},
|
|
|
+ {fields: ['id', 'name']},
|
|
|
+);
|
|
|
+console.log(product);
|
|
|
+// {
|
|
|
+// id: 2,
|
|
|
+// name: 'Mouse',
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+Create a document, including related data in the result.
|
|
|
+
|
|
|
+```js
|
|
|
+// assumes the Product model has a "category" relation
|
|
|
+// (the "include" option only affects the returned result)
|
|
|
+const product = await productRep.create(
|
|
|
+ {name: 'Keyboard', price: 75, categoryId: 10},
|
|
|
+ {include: 'category'},
|
|
|
+);
|
|
|
+console.log(product);
|
|
|
+// {
|
|
|
+// id: 3,
|
|
|
+// name: 'Keyboard',
|
|
|
+// price: 75,
|
|
|
+// categoryId: 10,
|
|
|
+// category: {id: 10, name: 'Electronics'}
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+### repository.replaceById
|
|
|
+
|
|
|
+Completely replaces an existing document by its identifier. All previous data
|
|
|
+in the document, except the identifier, is removed. Fields not provided in
|
|
|
+`data` will be absent from the final document (unless they have a default
|
|
|
+value).
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+replaceById(
|
|
|
+ id: IdType,
|
|
|
+ data: WithoutId<FlatData, IdName>,
|
|
|
+ filter?: ItemFilterClause<FlatData>,
|
|
|
+): Promise<FlatData>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Replace a document by its identifier.
|
|
|
+
|
|
|
+```js
|
|
|
+// original document
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Laptop',
|
|
|
+// price: 1200,
|
|
|
+// inStock: true
|
|
|
+// }
|
|
|
+
|
|
|
+const updatedProduct = await productRep.replaceById(1, {
|
|
|
+ name: 'Laptop Pro',
|
|
|
+ price: 1500,
|
|
|
+});
|
|
|
+console.log(updatedProduct);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Laptop Pro',
|
|
|
+// price: 1500
|
|
|
+// }
|
|
|
+// the "inStock" property is removed
|
|
|
+```
|
|
|
+
|
|
|
+### repository.replaceOrCreate
|
|
|
+
|
|
|
+Replaces an existing document if the provided data includes an identifier that
|
|
|
+already exists in the collection. Otherwise, if the identifier is not
|
|
|
+provided or not found, it creates a new document.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+replaceOrCreate(
|
|
|
+ data: WithOptionalId<FlatData, IdName>,
|
|
|
+ filter?: ItemFilterClause<FlatData>,
|
|
|
+): Promise<FlatData>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Create a new document if `id: 3` does not exist.
|
|
|
+
|
|
|
+```js
|
|
|
+const product = await productRep.replaceOrCreate({
|
|
|
+ id: 3,
|
|
|
+ name: 'Keyboard',
|
|
|
+ price: 75,
|
|
|
+});
|
|
|
+console.log(product);
|
|
|
+// {
|
|
|
+// id: 3,
|
|
|
+// name: 'Keyboard',
|
|
|
+// price: 75,
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+Replace an existing document with `id: 1`.
|
|
|
+
|
|
|
+```js
|
|
|
+const updatedProduct = await productRep.replaceOrCreate({
|
|
|
+ id: 1,
|
|
|
+ name: 'Laptop Pro',
|
|
|
+ price: 1500,
|
|
|
+});
|
|
|
+console.log(updatedProduct);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Laptop Pro',
|
|
|
+// price: 1500,
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+### repository.patchById
|
|
|
+
|
|
|
+Partially updates an existing document by its identifier, modifying only the
|
|
|
+provided fields. The other fields of the document remain unchanged.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+patchById(
|
|
|
+ id: IdType,
|
|
|
+ data: PartialWithoutId<FlatData, IdName>,
|
|
|
+ filter?: ItemFilterClause<FlatData>,
|
|
|
+): Promise<FlatData>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Partially update a document by its identifier.
|
|
|
+
|
|
|
+```js
|
|
|
+// original document with id: 1
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Laptop Pro',
|
|
|
+// price: 1500
|
|
|
+// }
|
|
|
+
|
|
|
+const updatedProduct = await productRep.patchById(1, {
|
|
|
+ price: 1450,
|
|
|
+});
|
|
|
+console.log(updatedProduct);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'Laptop Pro',
|
|
|
+// price: 1450
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+### repository.patch
|
|
|
+
|
|
|
+Partially updates one or more documents that match the `where` conditions.
|
|
|
+Only the provided fields are changed; the rest remain unchanged. Returns the
|
|
|
+number of updated documents. If `where` is not specified, it updates all
|
|
|
+documents in the collection.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+patch(
|
|
|
+ data: PartialWithoutId<FlatData, IdName>,
|
|
|
+ where?: WhereClause<FlatData>,
|
|
|
+): Promise<number>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Update documents based on a condition.
|
|
|
+
|
|
|
+```js
|
|
|
+// updates all products with a price less than 30
|
|
|
+const updatedCount = await productRep.patch(
|
|
|
+ {inStock: false},
|
|
|
+ {price: {lt: 30}},
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+Update all documents.
|
|
|
+
|
|
|
+```js
|
|
|
+// adds or updates the updatedAt field for all documents
|
|
|
+const totalCount = await productRep.patch({
|
|
|
+ updatedAt: new Date(),
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### repository.find
|
|
|
+
|
|
|
+Finds all documents that match the filter conditions and returns them as an
|
|
|
+array. If no filter is specified, it returns all documents in the
|
|
|
+collection.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+find(filter?: FilterClause<FlatData>): Promise<FlatData[]>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Find all documents.
|
|
|
+
|
|
|
+```js
|
|
|
+const allProducts = await productRep.find();
|
|
|
+```
|
|
|
+
|
|
|
+Find documents by a `where` condition.
|
|
|
+
|
|
|
+```js
|
|
|
+const cheapProducts = await productRep.find({
|
|
|
+ where: {price: {lt: 100}},
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Find with sorting and limiting the result set.
|
|
|
+
|
|
|
+```js
|
|
|
+const latestProducts = await productRep.find({
|
|
|
+ order: 'createdAt DESC',
|
|
|
+ limit: 10,
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### repository.findOne
|
|
|
+
|
|
|
+Finds the first document that matches the filter conditions. Returns
|
|
|
+`undefined` if no documents are found.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+findOne(
|
|
|
+ filter?: FilterClause<FlatData>,
|
|
|
+): Promise<FlatData | undefined>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Find a single document by a condition.
|
|
|
+
|
|
|
+```js
|
|
|
+const expensiveProduct = await productRep.findOne({
|
|
|
+ where: {price: {gt: 1000}},
|
|
|
+ order: 'price DESC',
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Handling the case where a document is not found.
|
|
|
+
|
|
|
+```js
|
|
|
+const product = await productRep.findOne({
|
|
|
+ where: {name: 'Non-existent Product'},
|
|
|
+});
|
|
|
+if (!product) {
|
|
|
+ console.log('Product not found.');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### repository.findById
|
|
|
+
|
|
|
+Finds a single document by its unique identifier. Throws an error if the
|
|
|
+document is not found.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+findById(
|
|
|
+ id: IdType,
|
|
|
+ filter?: ItemFilterClause<FlatData>,
|
|
|
+): Promise<FlatData>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Find a document by `id`.
|
|
|
+
|
|
|
+```js
|
|
|
+try {
|
|
|
+ const product = await productRep.findById(1);
|
|
|
+ console.log(product);
|
|
|
+} catch (error) {
|
|
|
+ console.error('Product with id 1 is not found.');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Find a document including related data.
|
|
|
+
|
|
|
+```js
|
|
|
+const product = await productRep.findById(1, {
|
|
|
+ include: 'category',
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### repository.delete
|
|
|
+
|
|
|
+Deletes one or more documents that match the `where` conditions. Returns the
|
|
|
+number of deleted documents. If `where` is not specified, it deletes all
|
|
|
+documents in the collection.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+delete(where?: WhereClause<FlatData>): Promise<number>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Delete documents based on a condition.
|
|
|
+
|
|
|
+```js
|
|
|
+const deletedCount = await productRep.delete({
|
|
|
+ inStock: false,
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Delete all documents.
|
|
|
+
|
|
|
+```js
|
|
|
+const totalCount = await productRep.delete();
|
|
|
+```
|
|
|
+
|
|
|
+### repository.deleteById
|
|
|
+
|
|
|
+Deletes a single document by its unique identifier. Returns `true` if the
|
|
|
+document was found and deleted, otherwise `false`.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+deleteById(id: IdType): Promise<boolean>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Delete a document by `id`.
|
|
|
+
|
|
|
+```js
|
|
|
+const wasDeleted = await productRep.deleteById(1);
|
|
|
+if (wasDeleted) {
|
|
|
+ console.log('The document was deleted.');
|
|
|
+} else {
|
|
|
+ console.log('No document found to delete.');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### repository.exists
|
|
|
+
|
|
|
+Checks for the existence of a document with the specified identifier.
|
|
|
+Returns `true` if the document exists, otherwise `false`.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+exists(id: IdType): Promise<boolean>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Check if a document exists by `id`.
|
|
|
+
|
|
|
+```js
|
|
|
+const productExists = await productRep.exists(1);
|
|
|
+if (productExists) {
|
|
|
+ console.log('A document with id 1 exists.');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### repository.count
|
|
|
+
|
|
|
+Counts the number of documents that match the `where` conditions. If `where`
|
|
|
+is not specified, it returns the total number of documents in the collection.
|
|
|
+
|
|
|
+Signature:
|
|
|
+
|
|
|
+```ts
|
|
|
+count(where?: WhereClause<FlatData>): Promise<number>;
|
|
|
+```
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Count documents based on a condition.
|
|
|
+
|
|
|
+```js
|
|
|
+const cheapCount = await productRep.count({
|
|
|
+ price: {lt: 100},
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Count all documents.
|
|
|
+
|
|
|
+```js
|
|
|
+const totalCount = await productRep.count();
|
|
|
+```
|
|
|
+
|
|
|
+## Filtering
|
|
|
+
|
|
|
+Some repository methods accept a settings object that affects the returned
|
|
|
+result. The `find` method's first parameter has the widest set of these
|
|
|
+settings, expecting an object with the options listed below.
|
|
|
+
|
|
|
+- `where: object` conditions to filter documents by their properties;
|
|
|
+- `order: string|string[]` sort by specified properties;
|
|
|
+- `limit: number` limit the number of documents;
|
|
|
+- `skip: number` skip documents (for pagination);
|
|
|
+- `fields: string|string[]` select necessary model properties;
|
|
|
+- `include: object` include related data in the result;
|
|
|
+
|
|
|
+**Example**
|
|
|
+
|
|
|
+```js
|
|
|
+// the "find" repository method is used for the query,
|
|
|
+// with a filter object passed as the first argument
|
|
|
+const news = await newsRepository.find({
|
|
|
+ where: {
|
|
|
+ title: {like: '%Moscow%'},
|
|
|
+ publishedAt: {gte: '2025-10-15T00:00:00.000Z'},
|
|
|
+ tags: {inq: ['world', 'politic']},
|
|
|
+ hidden: false,
|
|
|
+ },
|
|
|
+ order: 'publishedAt DESC',
|
|
|
+ limit: 12,
|
|
|
+ skip: 24,
|
|
|
+ fields: ['title', 'annotation', 'body'],
|
|
|
+ include: ['author', 'category'],
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+### where
|
|
|
+
|
|
|
+This parameter accepts an object with query conditions and supports the
|
|
|
+following set of comparison operators.
|
|
|
+
|
|
|
+- [Search by value (shorthand)](#search-by-value-shorthand)
|
|
|
+- [`eq`](#eq-strict-equality) (strict equality)
|
|
|
+- [`neq`](#neq-inequality) (inequality)
|
|
|
+- [`gt`](#gt-greater-than) (greater than)
|
|
|
+- [`lt`](#lt-less-than) (less than)
|
|
|
+- [`gte`](#gte-greater-than-or-equal-to) (greater than or equal to)
|
|
|
+- [`lte`](#lte-less-than-or-equal-to) (less than or equal to)
|
|
|
+- [`inq`](#inq-in-a-list) (in a list)
|
|
|
+- [`nin`](#nin-not-in-a-list) (not in a list)
|
|
|
+- [`between`](#between-range) (range)
|
|
|
+- [`exists`](#exists-property-existence) (property existence)
|
|
|
+- [`like`](#like-pattern-matching) (pattern matching)
|
|
|
+- [`nlike`](#nlike-excluding-pattern) (excluding pattern)
|
|
|
+- [`ilike`](#ilike-case-insensitive-pattern) (case-insensitive pattern)
|
|
|
+- [`nilike`](#nilike-case-insensitive-excluding-pattern) (case-insensitive excluding pattern)
|
|
|
+- [`regexp`](#regexp-regular-expression) (regular expression)
|
|
|
+
|
|
|
+Conditions can be combined with logical operators:
|
|
|
+
|
|
|
+- [`and`](#and-logical-and) (logical AND)
|
|
|
+- [`or`](#or-logical-or) (logical OR)
|
|
|
+
|
|
|
+#### Search by value (shorthand)
|
|
|
+
|
|
|
+Finds documents where the value of the specified property is exactly equal
|
|
|
+to the provided value. This is a shorthand for the `{eq: ...}` operator.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds all documents where age is 21
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: 21,
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `eq` (strict equality)
|
|
|
+
|
|
|
+Finds documents where the property value is equal to the specified value.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds all documents where age is 21
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: {eq: 21},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `neq` (inequality)
|
|
|
+
|
|
|
+Finds documents where the property value is not equal to the specified value.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds all documents where age is not 21
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: {neq: 21},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `gt` (greater than)
|
|
|
+
|
|
|
+Finds documents where the property value is strictly greater than the
|
|
|
+specified value.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents where age is greater than 30
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: {gt: 30},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `lt` (less than)
|
|
|
+
|
|
|
+Finds documents where the property value is strictly less than the
|
|
|
+specified value.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents where age is less than 30
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: {lt: 30},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `gte` (greater than or equal to)
|
|
|
+
|
|
|
+Finds documents where the property value is greater than or equal to the
|
|
|
+specified value.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents where age is greater than or equal to 30
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: {gte: 30},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `lte` (less than or equal to)
|
|
|
+
|
|
|
+Finds documents where the property value is less than or equal to the
|
|
|
+specified value.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents where age is less than or equal to 30
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: {lte: 30},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `inq` (in a list)
|
|
|
+
|
|
|
+Finds documents where the property value matches one of the values in the
|
|
|
+provided array.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents where name is 'John' or 'Mary'
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {inq: ['John', 'Mary']},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `nin` (not in a list)
|
|
|
+
|
|
|
+Finds documents where the property value is not in the provided array.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds all documents except those where name is 'John' or 'Mary'
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {nin: ['John', 'Mary']},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `between` (range)
|
|
|
+
|
|
|
+Finds documents where the property value is within the specified range
|
|
|
+(inclusive).
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents where age is between 20 and 30, inclusive
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ age: {between: [20, 30]},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `exists` (property existence)
|
|
|
+
|
|
|
+Checks for the presence or absence of a property in a document. Does not
|
|
|
+check the property's value.
|
|
|
+
|
|
|
+- `true` the property must exist (even if its value is `null`);
|
|
|
+- `false` the property must be absent;
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents that have the 'nickname' property
|
|
|
+const res1 = await rep.find({
|
|
|
+ where: {
|
|
|
+ nickname: {exists: true},
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// finds documents that do not have the 'nickname' property
|
|
|
+const res2 = await rep.find({
|
|
|
+ where: {
|
|
|
+ nickname: {exists: false},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `like` (pattern matching)
|
|
|
+
|
|
|
+Performs a case-sensitive pattern match (see [more details](#pattern-matching-operators)).
|
|
|
+
|
|
|
+```js
|
|
|
+// finds {name: 'John Doe'}, but not {name: 'john doe'}
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {like: 'John%'},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `nlike` (excluding pattern)
|
|
|
+
|
|
|
+Finds documents that do not match the case-sensitive pattern
|
|
|
+(see [more details](#pattern-matching-operators)).
|
|
|
+
|
|
|
+```js
|
|
|
+// finds everything except names starting with 'John'
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {nlike: 'John%'},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `ilike` (case-insensitive pattern)
|
|
|
+
|
|
|
+Performs a case-insensitive pattern match (see [more details](#pattern-matching-operators)).
|
|
|
+
|
|
|
+```js
|
|
|
+// finds both {name: 'John Doe'} and {name: 'john doe'}
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {ilike: 'john%'},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `nilike` (case-insensitive excluding pattern)
|
|
|
+
|
|
|
+Finds strings that do not match the pattern, case-insensitively
|
|
|
+(see [more details](#pattern-matching-operators)).
|
|
|
+
|
|
|
+```js
|
|
|
+// finds everything except names starting with 'John' or 'john'
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {nilike: 'john%'},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `regexp` (regular expression)
|
|
|
+
|
|
|
+Finds documents where the string property's value matches the specified
|
|
|
+regular expression. Can be passed as a string or a `RegExp` object.
|
|
|
+
|
|
|
+```js
|
|
|
+// finds documents where name starts with 'J'
|
|
|
+const res1 = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {regexp: '^J'},
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// finds documents where name starts with 'J' or 'j' (case-insensitive)
|
|
|
+const res2 = await rep.find({
|
|
|
+ where: {
|
|
|
+ name: {regexp: '^j', flags: 'i'},
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `and` (logical AND)
|
|
|
+
|
|
|
+Combines multiple conditions into an array, requiring every condition to be
|
|
|
+met.
|
|
|
+
|
|
|
+```javascript
|
|
|
+// finds documents where surname is 'Smith' AND age is 21
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ and: [
|
|
|
+ {surname: 'Smith'},
|
|
|
+ {age: 21}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### `or` (logical OR)
|
|
|
+
|
|
|
+Combines multiple conditions into an array, requiring at least one of them
|
|
|
+to be met.
|
|
|
+
|
|
|
+```javascript
|
|
|
+// finds documents where name is 'James' OR age is greater than 30
|
|
|
+const res = await rep.find({
|
|
|
+ where: {
|
|
|
+ or: [
|
|
|
+ {name: 'James'},
|
|
|
+ {age: {gt: 30}}
|
|
|
+ ],
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### Pattern Matching Operators
|
|
|
+
|
|
|
+The `like`, `nlike`, `ilike`, and `nilike` operators are designed for filtering
|
|
|
+string properties based on pattern matching, similar to the `LIKE` operator
|
|
|
+in SQL. They allow finding values that match a certain structure using
|
|
|
+special characters.
|
|
|
+
|
|
|
+**`%`** matches any sequence of zero or more characters:
|
|
|
+
|
|
|
+- `'A%'` finds all strings starting with "A";
|
|
|
+- `'%a'` finds all strings ending with "a";
|
|
|
+- `'%word%'` finds all strings containing "word" anywhere;
|
|
|
+
|
|
|
+**`_`** matches exactly one of any character:
|
|
|
+
|
|
|
+- `'c_t'` finds "cat", "cot", but not "cart" or "ct";
|
|
|
+- `'cat_'` finds "cats", "cat1", but not "cat" or "catch";
|
|
|
+
|
|
|
+To find the literal characters `%` or `_`, they must be escaped with a
|
|
|
+backslash `\`:
|
|
|
+
|
|
|
+- `'100\%'` finds the string "100%";
|
|
|
+- `'file\_name'` finds the string "file_name";
|
|
|
+- `'path\\to'` finds the string "path\to";
|
|
|
+
|
|
|
+### order
|
|
|
+
|
|
|
+This parameter sorts the result set by the specified model properties. You can
|
|
|
+specify descending order with the `DESC` postfix.
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Sort by the `createdAt` field.
|
|
|
+
|
|
|
+```js
|
|
|
+const res = await rep.find({
|
|
|
+ order: 'createdAt',
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Sort by the `createdAt` field in descending order.
|
|
|
+
|
|
|
+```js
|
|
|
+const res = await rep.find({
|
|
|
+ order: 'createdAt DESC',
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Sort by multiple properties in different directions.
|
|
|
+
|
|
|
+```js
|
|
|
+const res = await rep.find({
|
|
|
+ order: [
|
|
|
+ 'title',
|
|
|
+ 'price ASC',
|
|
|
+ 'featured DESC',
|
|
|
+ ],
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+*i. The `ASC` sort direction is optional.*
|
|
|
+
|
|
|
+### include
|
|
|
+
|
|
|
+This parameter includes related documents in the method's result. The names
|
|
|
+of the included relations must be defined in the current model
|
|
|
+(see [Relations](#relations)).
|
|
|
+
|
|
|
+**Examples**
|
|
|
+
|
|
|
+Include a relation by its name.
|
|
|
+
|
|
|
+```js
|
|
|
+const res = await rep.find({
|
|
|
+ include: 'city',
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Include nested relations.
|
|
|
+
|
|
|
+```js
|
|
|
+const res = await rep.find({
|
|
|
+ include: {
|
|
|
+ city: 'country',
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Include multiple relations using an array.
|
|
|
+
|
|
|
+```js
|
|
|
+const res = await rep.find({
|
|
|
+ include: [
|
|
|
+ 'city',
|
|
|
+ 'address',
|
|
|
+ 'employees'
|
|
|
+ ],
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Using filtering for included documents.
|
|
|
+
|
|
|
+```js
|
|
|
+const res = await rep.find({
|
|
|
+ include: {
|
|
|
+ relation: 'employees', // relation name
|
|
|
+ scope: { // filter for "employees" documents
|
|
|
+ where: {hidden: false}, // query conditions
|
|
|
+ order: 'id', // document order
|
|
|
+ limit: 10, // limit the number of documents
|
|
|
+ skip: 5, // skip documents
|
|
|
+ fields: ['name', 'surname'], // only these fields
|
|
|
+ include: 'city', // include relations for "employees"
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Relations
|
|
|
+
|
|
|
+Relations describe relationships between models, allowing for automatic
|
|
|
+embedding of related data using the `include` option in repository methods.
|
|
|
+The example below shows automatic relation resolution when using the
|
|
|
+`findById` method.
|
|
|
+
|
|
|
+```
|
|
|
+ Role (role)
|
|
|
+ ┌────────────────────┐
|
|
|
+ │ id: 3 <──────────│────┐
|
|
|
+ │ name: 'Manager' │ │
|
|
|
+ └────────────────────┘ │
|
|
|
+ │
|
|
|
+ User (user) │
|
|
|
+ ┌────────────────────────┐ │
|
|
|
+ │ id: 1 │ │
|
|
|
+ │ name: 'John Doe' │ │
|
|
|
+ │ roleId: 3 ──────────│────┘
|
|
|
+ │ cityId: 24 ──────────│────┐
|
|
|
+ └────────────────────────┘ │
|
|
|
+ │
|
|
|
+ City (city) │
|
|
|
+ ┌────────────────────┐ │
|
|
|
+ │ id: 24 <─────────│────┘
|
|
|
+ │ name: 'Moscow' │
|
|
|
+ └────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+```js
|
|
|
+// query a document from the "users" collection,
|
|
|
+// including related data (role and city)
|
|
|
+const user = await userRep.findById(1, {
|
|
|
+ include: ['role', 'city'],
|
|
|
+});
|
|
|
+
|
|
|
+console.log(user);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'John Doe',
|
|
|
+// roleId: 3,
|
|
|
+// role: {
|
|
|
+// id: 3,
|
|
|
+// name: 'Manager'
|
|
|
+// },
|
|
|
+// cityId: 24,
|
|
|
+// city: {
|
|
|
+// id: 24,
|
|
|
+// name: 'Moscow'
|
|
|
+// }
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+### Defining a Relation
|
|
|
+
|
|
|
+The `relations` property in a model definition accepts an object where keys
|
|
|
+are relation names and values are their parameters. The relation name can
|
|
|
+then be used in the `include` option of repository methods.
|
|
|
+
|
|
|
+```js
|
|
|
+import {
|
|
|
+ DataType,
|
|
|
+ RelationType,
|
|
|
+ DatabaseSchema,
|
|
|
+} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ datasource: 'memory',
|
|
|
+ properties: {
|
|
|
+ name: DataType.STRING,
|
|
|
+ },
|
|
|
+ relations: {
|
|
|
+ // relation role -> parameters
|
|
|
+ role: {
|
|
|
+ type: RelationType.BELONGS_TO,
|
|
|
+ model: 'role',
|
|
|
+ },
|
|
|
+ // relation city -> parameters
|
|
|
+ city: {
|
|
|
+ type: RelationType.BELONGS_TO,
|
|
|
+ model: 'city',
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+**Core Parameters**
|
|
|
+
|
|
|
+- `type: string` the relation type (required);
|
|
|
+- `model: string` the target model name (required for some types);
|
|
|
+- `foreignKey: string` the property in the current model for the target ID;
|
|
|
+
|
|
|
+*i. For Belongs To and References Many types, the `foreignKey` parameter
|
|
|
+can be omitted as it's automatically generated based on the relation name.*
|
|
|
+
|
|
|
+**Polymorphic Mode**
|
|
|
+
|
|
|
+- `polymorphic: boolean|string` declares a polymorphic relation;
|
|
|
+- `discriminator: string` the property in the current model for the target's name;
|
|
|
+
|
|
|
+*i. Polymorphic mode allows dynamically determining the target model by its
|
|
|
+name, which is stored in the document's discriminator property.*
|
|
|
+
|
|
|
+### Relation Types
|
|
|
+
|
|
|
+- [Belongs To](#belongs-to)
|
|
|
+ The current model references the target model by its ID.
|
|
|
+ `type: "belongsTo"` or `type: RelationType.BELONGS_TO`
|
|
|
+
|
|
|
+- [Has One](#has-one)
|
|
|
+ The inverse of `belongsTo` for a "one-to-one" relationship.
|
|
|
+ `type: "hasOne"` or `type: RelationType.HAS_ONE`
|
|
|
+
|
|
|
+- [Has Many](#has-many)
|
|
|
+ The inverse of `belongsTo` for a "one-to-many" relationship.
|
|
|
+ `type: "hasMany"` or `type: RelationType.HAS_MANY`
|
|
|
+
|
|
|
+- [References Many](#references-many)
|
|
|
+ The current model references the target model via an array of IDs.
|
|
|
+ `type: "referencesMany"` or `type: RelationType.REFERENCES_MANY`
|
|
|
+
|
|
|
+Polymorphic versions:
|
|
|
+
|
|
|
+- [Belongs To (Polymorphic)](#belongs-to-polymorphic-version)
|
|
|
+- [Has One (Polymorphic)](#has-one-polymorphic-version)
|
|
|
+- [Has Many (Polymorphic)](#has-many-polymorphic-version)
|
|
|
+
|
|
|
+The `type` parameter in a relation definition accepts a string with the type
|
|
|
+name. To avoid typos, it's recommended to use the constants from the
|
|
|
+`RelationType` object listed below.
|
|
|
+
|
|
|
+- `RelationType.BELONGS_TO`
|
|
|
+- `RelationType.HAS_ONE`
|
|
|
+- `RelationType.HAS_MANY`
|
|
|
+- `RelationType.REFERENCES_MANY`
|
|
|
+
|
|
|
+#### Belongs To
|
|
|
+
|
|
|
+The current model references the target model by its identifier.
|
|
|
+
|
|
|
+```
|
|
|
+ Current (user) Target (role)
|
|
|
+┌─────────────────────────┐ ┌─────────────────────────┐
|
|
|
+│ id: 1 │ ┌───│─> id: 5 │
|
|
|
+│ roleId: 5 ───────────│───┤ │ ... │
|
|
|
+│ ... │ │ └─────────────────────────┘
|
|
|
+└─────────────────────────┘ │
|
|
|
+┌─────────────────────────┐ │
|
|
|
+│ id: 2 │ │
|
|
|
+│ roleId: 5 ───────────│───┘
|
|
|
+│ ... │
|
|
|
+└─────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition:
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ relations: {
|
|
|
+ role: { // relation name
|
|
|
+ type: RelationType.BELONGS_TO, // current model references target
|
|
|
+ model: 'role', // target model name
|
|
|
+ foreignKey: 'roleId', // foreign key (optional)
|
|
|
+ // if "foreignKey" is not specified, the foreign key property
|
|
|
+ // is formed based on the relation name with an "Id" postfix
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Example:
|
|
|
+
|
|
|
+```js
|
|
|
+import {
|
|
|
+ DataType,
|
|
|
+ RelationType,
|
|
|
+ DatabaseSchema,
|
|
|
+} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+const dbs = new DatabaseSchema();
|
|
|
+
|
|
|
+// datasource
|
|
|
+dbs.defineDatasource({
|
|
|
+ name: 'myDb',
|
|
|
+ adapter: 'memory',
|
|
|
+});
|
|
|
+
|
|
|
+// role model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'role',
|
|
|
+ datasource: 'myDb',
|
|
|
+ properties: {
|
|
|
+ name: DataType.STRING,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// user model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'user',
|
|
|
+ datasource: 'myDb',
|
|
|
+ properties: {
|
|
|
+ name: DataType.STRING,
|
|
|
+ roleId: DataType.NUMBER, // optional
|
|
|
+ },
|
|
|
+ relations: {
|
|
|
+ role: {
|
|
|
+ type: RelationType.BELONGS_TO,
|
|
|
+ model: 'role',
|
|
|
+ foreignKey: 'roleId', // optional
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// create a role
|
|
|
+const roleRep = dbs.getRepository('role');
|
|
|
+const role = await roleRep.create({
|
|
|
+ id: 5,
|
|
|
+ name: 'Manager',
|
|
|
+});
|
|
|
+console.log(role);
|
|
|
+// {
|
|
|
+// id: 5,
|
|
|
+// name: 'Manager'
|
|
|
+// }
|
|
|
+
|
|
|
+// create a user
|
|
|
+const userRep = dbs.getRepository('user');
|
|
|
+const user = await userRep.create({
|
|
|
+ id: 1,
|
|
|
+ name: 'John Doe',
|
|
|
+ roleId: role.id,
|
|
|
+});
|
|
|
+console.log(user);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'John Doe',
|
|
|
+// roleId: 5
|
|
|
+// }
|
|
|
+
|
|
|
+// fetch user with related role ("include" option)
|
|
|
+const userWithRole = await userRep.findById(user.id, {include: 'role'});
|
|
|
+console.log(userWithRole);
|
|
|
+// {
|
|
|
+// id: 1,
|
|
|
+// name: 'John Doe',
|
|
|
+// roleId: 5,
|
|
|
+// role: {
|
|
|
+// id: 5,
|
|
|
+// name: 'Manager'
|
|
|
+// }
|
|
|
+// }
|
|
|
+```
|
|
|
+
|
|
|
+#### Has One
|
|
|
+
|
|
|
+The inverse of `belongsTo` for a "one-to-one" relationship.
|
|
|
+
|
|
|
+```
|
|
|
+ Current (profile) Target (user)
|
|
|
+┌─────────────────────────┐ ┌─────────────────────────┐
|
|
|
+│ id: 5 <──────────────│───┐ │ id: 1 │
|
|
|
+│ ... │ └───│── profileId: 5 │
|
|
|
+└─────────────────────────┘ │ ... │
|
|
|
+ └─────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition:
|
|
|
+
|
|
|
+```js
|
|
|
+// dbs.defineModel({
|
|
|
+// name: 'user',
|
|
|
+// relations: {
|
|
|
+// profile: {
|
|
|
+// type: RelationType.BELONGS_TO,
|
|
|
+// model: 'profile',
|
|
|
+// },
|
|
|
+// },
|
|
|
+// });
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'profile',
|
|
|
+ relations: {
|
|
|
+ user: { // relation name
|
|
|
+ type: RelationType.HAS_ONE, // target model references current
|
|
|
+ model: 'user', // target model name
|
|
|
+ foreignKey: 'profileId', // foreign key from target to current
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### Has Many
|
|
|
+
|
|
|
+The inverse of `belongsTo` for a "one-to-many" relationship.
|
|
|
+
|
|
|
+```
|
|
|
+ Current (role) Target (user)
|
|
|
+┌─────────────────────────┐ ┌─────────────────────────┐
|
|
|
+│ id: 5 <──────────────│───┐ │ id: 1 │
|
|
|
+│ ... │ ├───│── roleId: 5 │
|
|
|
+└─────────────────────────┘ │ │ ... │
|
|
|
+ │ └─────────────────────────┘
|
|
|
+ │ ┌─────────────────────────┐
|
|
|
+ │ │ id: 2 │
|
|
|
+ └───│── roleId: 5 │
|
|
|
+ │ ... │
|
|
|
+ └─────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition:
|
|
|
+
|
|
|
+```js
|
|
|
+// dbs.defineModel({
|
|
|
+// name: 'user',
|
|
|
+// relations: {
|
|
|
+// role: {
|
|
|
+// type: RelationType.BELONGS_TO,
|
|
|
+// model: 'role',
|
|
|
+// },
|
|
|
+// },
|
|
|
+// });
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'role',
|
|
|
+ relations: {
|
|
|
+ users: { // relation name
|
|
|
+ type: RelationType.HAS_MANY, // target model references current
|
|
|
+ model: 'user', // target model name
|
|
|
+ foreignKey: 'roleId', // foreign key in the target model
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### References Many
|
|
|
+
|
|
|
+The current model references the target model via an array of identifiers.
|
|
|
+
|
|
|
+```
|
|
|
+ Current (article) Target (category)
|
|
|
+┌─────────────────────────┐ ┌─────────────────────────┐
|
|
|
+│ id: 1 │ ┌───│─> id: 5 │
|
|
|
+│ categoryIds: [5, 6] ──│───┤ │ ... │
|
|
|
+│ ... │ │ └─────────────────────────┘
|
|
|
+└─────────────────────────┘ │ ┌─────────────────────────┐
|
|
|
+ └───│─> id: 6 │
|
|
|
+ │ ... │
|
|
|
+ └─────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition:
|
|
|
+
|
|
|
+```js
|
|
|
+// dbs.defineModel({name: 'category', ...
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'article',
|
|
|
+ relations: {
|
|
|
+ categories: { // relation name
|
|
|
+ type: RelationType.REFERENCES_MANY, // relation via array of IDs
|
|
|
+ model: 'category', // target model name
|
|
|
+ foreignKey: 'categoryIds', // foreign key (optional)
|
|
|
+ // if "foreignKey" is not specified, the foreign key property
|
|
|
+ // is formed based on the relation name with an "Ids" postfix
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### Belongs To (Polymorphic Version)
|
|
|
+
|
|
|
+The current model references a target model by its identifier. The target
|
|
|
+model's name is determined by a discriminator property.
|
|
|
+
|
|
|
+```
|
|
|
+ Current (file) ┌──────> Target 1 (letter)
|
|
|
+┌─────────────────────────────┐ │ ┌─────────────────────────┐
|
|
|
+│ id: 1 │ │ ┌──│─> id: 10 │
|
|
|
+│ referenceType: 'letter' ─│──┘ │ │ ... │
|
|
|
+│ referenceId: 10 ─────────│────┘ └─────────────────────────┘
|
|
|
+└─────────────────────────────┘
|
|
|
+ ┌──────> Target 2 (user)
|
|
|
+┌─────────────────────────────┐ │ ┌─────────────────────────┐
|
|
|
+│ id: 2 │ │ ┌──│─> id: 5 │
|
|
|
+│ referenceType: 'user' ───│──┘ │ │ ... │
|
|
|
+│ referenceId: 5 ──────────│────┘ └─────────────────────────┘
|
|
|
+└─────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition:
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'file',
|
|
|
+ relations: {
|
|
|
+ reference: { // relation name
|
|
|
+ type: RelationType.BELONGS_TO, // current model references target
|
|
|
+ // polymorphic mode allows storing the target model's name
|
|
|
+ // in a discriminator property, which is formed based on
|
|
|
+ // the relation name with a "Type" postfix, in this case,
|
|
|
+ // the target model's name is stored in "referenceType",
|
|
|
+ // and the document's ID is in "referenceId"
|
|
|
+ polymorphic: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition with explicit properties:
|
|
|
+
|
|
|
+```js
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'file',
|
|
|
+ relations: {
|
|
|
+ reference: { // relation name
|
|
|
+ type: RelationType.BELONGS_TO, // current model references target
|
|
|
+ polymorphic: true, // target model name is in a discriminator
|
|
|
+ foreignKey: 'referenceId', // property for the target's ID
|
|
|
+ discriminator: 'referenceType', // property for the target's name
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### Has One (Polymorphic Version)
|
|
|
+
|
|
|
+The inverse of a polymorphic `belongsTo` for a "one-to-one" relationship.
|
|
|
+
|
|
|
+```
|
|
|
+ Current (company) <───────┐ Target (license)
|
|
|
+┌─────────────────────────┐ │ ┌─────────────────────────┐
|
|
|
+│ id: 10 <─────────────│──┐ │ │ id: 1 │
|
|
|
+│ ... │ │ └──│── ownerType: 'company' │
|
|
|
+└─────────────────────────┘ └────│── ownerId: 10 │
|
|
|
+ └─────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition specifying the target's relation name:
|
|
|
+
|
|
|
+```js
|
|
|
+// dbs.defineModel({
|
|
|
+// name: 'license',
|
|
|
+// relations: {
|
|
|
+// owner: {
|
|
|
+// type: RelationType.BELONGS_TO,
|
|
|
+// polymorphic: true,
|
|
|
+// },
|
|
|
+// },
|
|
|
+// });
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'company',
|
|
|
+ relations: {
|
|
|
+ license: { // relation name
|
|
|
+ type: RelationType.HAS_ONE, // target model references current
|
|
|
+ model: 'license', // target model name
|
|
|
+ polymorphic: 'owner', // polymorphic relation name in target model
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition specifying the target model's properties:
|
|
|
+
|
|
|
+```js
|
|
|
+// dbs.defineModel({
|
|
|
+// name: 'license',
|
|
|
+// relations: {
|
|
|
+// owner: {
|
|
|
+// type: RelationType.BELONGS_TO,
|
|
|
+// polymorphic: true,
|
|
|
+// foreignKey: 'ownerId',
|
|
|
+// discriminator: 'ownerType',
|
|
|
+// },
|
|
|
+// },
|
|
|
+// });
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'company',
|
|
|
+ relations: {
|
|
|
+ license: { // relation name
|
|
|
+ type: RelationType.HAS_ONE, // target model references current
|
|
|
+ model: 'license', // target model name
|
|
|
+ polymorphic: true, // current model name is in discriminator
|
|
|
+ foreignKey: 'ownerId', // property in target for current's ID
|
|
|
+ discriminator: 'ownerType', // property in target for current's name
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### Has Many (Polymorphic Version)
|
|
|
+
|
|
|
+The inverse of a polymorphic `belongsTo` for a "one-to-many" relationship.
|
|
|
+
|
|
|
+```
|
|
|
+ Current (letter) <─────────┐ Target (file)
|
|
|
+┌──────────────────────────┐ │ ┌────────────────────────────┐
|
|
|
+│ id: 10 <──────────────│──┐ │ │ id: 1 │
|
|
|
+│ ... │ │ ├──│── referenceType: 'letter' │
|
|
|
+└──────────────────────────┘ ├─│──│── referenceId: 10 │
|
|
|
+ │ │ └────────────────────────────┘
|
|
|
+ │ │ ┌────────────────────────────┐
|
|
|
+ │ │ │ id: 2 │
|
|
|
+ │ └──│── referenceType: 'letter' │
|
|
|
+ └────│── referenceId: 10 │
|
|
|
+ └────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition specifying the target's relation name:
|
|
|
+
|
|
|
+```js
|
|
|
+// dbs.defineModel({
|
|
|
+// name: 'file',
|
|
|
+// relations: {
|
|
|
+// reference: {
|
|
|
+// type: RelationType.BELONGS_TO,
|
|
|
+// polymorphic: true,
|
|
|
+// },
|
|
|
+// },
|
|
|
+// });
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'letter',
|
|
|
+ relations: {
|
|
|
+ attachments: { // relation name
|
|
|
+ type: RelationType.HAS_MANY, // target model references current
|
|
|
+ model: 'file', // target model name
|
|
|
+ polymorphic: 'reference', // polymorphic relation name in target
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+Relation definition specifying the target model's properties:
|
|
|
+
|
|
|
+```js
|
|
|
+// dbs.defineModel({
|
|
|
+// name: 'file',
|
|
|
+// relations: {
|
|
|
+// reference: {
|
|
|
+// type: RelationType.BELONGS_TO,
|
|
|
+// polymorphic: true,
|
|
|
+// foreignKey: 'referenceId',
|
|
|
+// discriminator: 'referenceType',
|
|
|
+// },
|
|
|
+// },
|
|
|
+// });
|
|
|
+
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'letter',
|
|
|
+ relations: {
|
|
|
+ attachments: { // relation name
|
|
|
+ type: RelationType.HAS_MANY, // target model references current
|
|
|
+ model: 'file', // target model name
|
|
|
+ polymorphic: true, // current model name is in discriminator
|
|
|
+ foreignKey: 'referenceId', // property in target for current's ID
|
|
|
+ discriminator: 'referenceType', // property in target for current's name
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+## Extending
|
|
|
+
|
|
|
+The `getRepository` method of a `DatabaseSchema` instance checks for an
|
|
|
+existing repository for the specified model and returns it. Otherwise, a new
|
|
|
+instance is created and cached for subsequent calls.
|
|
|
+
|
|
|
+```js
|
|
|
+import {Repository} from '@e22m4u/js-repository';
|
|
|
+import {DatabaseSchema} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+// const dbs = new DatabaseSchema();
|
|
|
+// dbs.defineDatasource ...
|
|
|
+// dbs.defineModel ...
|
|
|
+
|
|
|
+const rep1 = dbs.getRepository('model');
|
|
|
+const rep2 = dbs.getRepository('model');
|
|
|
+console.log(rep1 === rep2); // true
|
|
|
+```
|
|
|
+
|
|
|
+You can replace the default repository constructor using the `setRepositoryCtor`
|
|
|
+method of the `RepositoryRegistry` service, which is available in the
|
|
|
+`DatabaseSchema` instance's service container. After that, all new
|
|
|
+repositories will be created using the specified constructor instead of the
|
|
|
+default one.
|
|
|
+
|
|
|
+```js
|
|
|
+import {Repository} from '@e22m4u/js-repository';
|
|
|
+import {DatabaseSchema} from '@e22m4u/js-repository';
|
|
|
+import {RepositoryRegistry} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+class MyRepository extends Repository {
|
|
|
+ /*...*/
|
|
|
+}
|
|
|
+
|
|
|
+// const dbs = new DatabaseSchema();
|
|
|
+// dbs.defineDatasource ...
|
|
|
+// dbs.defineModel ...
|
|
|
+
|
|
|
+dbs.getService(RepositoryRegistry).setRepositoryCtor(MyRepository);
|
|
|
+const rep = dbs.getRepository('model');
|
|
|
+console.log(rep instanceof MyRepository); // true
|
|
|
+```
|
|
|
+
|
|
|
+*i. Since repository instances are cached, you should replace the
|
|
|
+constructor before calling the `getRepository` method.*
|
|
|
+
|
|
|
+## TypeScript
|
|
|
+
|
|
|
+Getting a typed repository with a specified model interface.
|
|
|
+
|
|
|
+```ts
|
|
|
+import {DataType} from '@e22m4u/js-repository';
|
|
|
+import {RelationType} from '@e22m4u/js-repository';
|
|
|
+import {DatabaseSchema} from '@e22m4u/js-repository';
|
|
|
+
|
|
|
+// const dbs = new DatabaseSchema();
|
|
|
+// dbs.defineDatasource ...
|
|
|
+
|
|
|
+// define the "city" model
|
|
|
+dbs.defineModel({
|
|
|
+ name: 'city',
|
|
|
+ datasource: 'myDatasource',
|
|
|
+ properties: {
|
|
|
+ name: DataType.STRING,
|
|
|
+ timeZone: DataType.STRING,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+// define the "city" interface
|
|
|
+interface City {
|
|
|
+ id: number;
|
|
|
+ name?: string;
|
|
|
+ timeZone?: string;
|
|
|
+}
|
|
|
+
|
|
|
+// when getting a repository for a model,
|
|
|
+// you can specify the document type
|
|
|
+const cityRep = dbs.getRepository<City>('city');
|
|
|
+
|
|
|
+// now, repository methods return
|
|
|
+// the City type instead of Record<string, unknown>
|
|
|
+const city: City = await cityRep.create({
|
|
|
+ name: 'Moscow',
|
|
|
+ timeZone: 'Europe/Moscow',
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+For defining models using TypeScript classes, it is recommended to use the
|
|
|
+specialized version of this module,
|
|
|
+[@e22m4u/ts-repository](https://www.npmjs.com/package/@e22m4u/ts-repository),
|
|
|
+which comes with a set of TypeScript decorators and additional tools for
|
|
|
+working in a TypeScript environment.
|
|
|
+
|
|
|
+## Tests
|
|
|
+
|
|
|
+```bash
|
|
|
+npm run test
|
|
|
+```
|
|
|
+
|
|
|
+## License
|
|
|
+
|
|
|
+MIT
|