Реализация репозитория для работы с базами данных
|
|
4 недель назад | |
|---|---|---|
| .husky | 1 год назад | |
| assets | 1 месяц назад | |
| dist | 1 месяц назад | |
| src | 1 месяц назад | |
| .c8rc | 2 лет назад | |
| .commitlintrc | 2 лет назад | |
| .editorconfig | 2 лет назад | |
| .gitignore | 1 год назад | |
| .mocharc.json | 4 месяцев назад | |
| .prettierrc | 2 лет назад | |
| LICENSE | 4 месяцев назад | |
| README-en.md | 4 недель назад | |
| README.md | 4 недель назад | |
| build-cjs.js | 1 год назад | |
| eslint.config.js | 2 месяцев назад | |
| package.json | 1 месяц назад | |
| tsconfig.json | 1 год назад |
Русский | English
An implementation of the Repository pattern for working with databases in Node.js.
npm install @e22m4u/js-repository
Optionally, install the required adapter.
| adapter | description | installation |
|---|---|---|
memory |
An in-process memory database | built-in |
mongodb |
MongoDB - a NoSQL database management system | npm |
The module supports both ESM and CommonJS standards.
ESM
import {DatabaseSchema} from '@e22m4u/js-repository';
CommonJS
const {DatabaseSchema} = require('@e22m4u/js-repository');
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.
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
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 │
└─────────────────────────┘
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 │
└─────────────────────────┘ └─────────────────────────┘
// 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
// }
// }
An instance of the DatabaseSchema class stores
datasource and model definitions.
Methods
defineDatasource(datasourceDef: object): this - add a
datasourcedefineModel(modelDef: object): this - add a modelgetRepository(modelName: string): Repository - get a
repositoryExamples
Import the class and create a schema instance.
import {DatabaseSchema} from '@e22m4u/js-repository';
const dbs = new DatabaseSchema();
Define a new datasource.
dbs.defineDatasource({
name: 'myDb', // name of the new datasource
adapter: 'memory', // chosen adapter
});
Define a new model.
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.
const productRep = dbs.getRepository('product');
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 nameadapter: string the chosen adapterExamples
Define a new datasource.
dbs.defineDatasource({
name: 'myDb', // name of the new datasource
adapter: 'memory', // chosen adapter
});
Passing additional parameters, using the MongoDB adapter as an example (*install*).
dbs.defineDatasource({
name: 'myDb',
adapter: 'mongodb',
// parameters for the "mongodb" adapter
host: '127.0.0.1',
port: 27017,
database: 'myDatabase',
});
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
fromtableName: string name of the collection in the
databasedatasource: string the chosen datasourceproperties: object property definitions (see Properties)relations: object relation definitions (see Relations)Examples
Define a model with properties of specified types.
dbs.defineModel({
name: 'user', // name of the new model
properties: { // model properties
name: DataType.STRING,
age: DataType.NUMBER,
},
});
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 allowedDataType.STRING only string valuesDataType.NUMBER only number valuesDataType.BOOLEAN only boolean valuesDataType.ARRAY only array valuesDataType.OBJECT only object valuesParameters
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
keycolumnName: string overrides the column namecolumnType: string column type (defined by the
adapter)required: boolean declares the property as
requireddefault: any default valuevalidate: string | Function | array | object see Validatorsunique: boolean | string check value for
uniquenessThe unique Parameter
If the unique parameter is true or
'strict', a strict uniqueness check is performed. In this
mode, 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, 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 checkunique: 'sparse' exclude empty
values from the checkunique: false | 'nonUnique' do not check for uniqueness
(default)You can use predefined constants as equivalents for the string values
strict, sparse, and
nonUnique.
PropertyUniqueness.STRICTPropertyUniqueness.SPARSEPropertyUniqueness.NON_UNIQUEExamples
Short-form property definition.
dbs.defineModel({
name: 'city',
properties: { // model properties
name: DataType.STRING, // property type "string"
population: DataType.NUMBER, // property type "number"
},
});
Full-form property definition.
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.
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 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 bypass validators as they have no payload.
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.
dbs.defineModel({
name: 'user',
properties: {
email: {
type: DataType.STRING,
validate: 'isEmail',
},
},
});
Using global validators as an array.
dbs.defineModel({
name: 'user',
properties: {
email: {
type: DataType.STRING,
validate: [
'isEmail',
'isLowerCase',
],
},
},
});
Using global validators with arguments.
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.
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,
},
},
},
});
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.
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.
// 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'},
},
},
},
});
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.
// 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.
// 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 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 are not passed to transformers as they have no payload.
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.
dbs.defineModel({
name: 'user',
properties: {
username: {
type: DataType.STRING,
transform: 'toLowerCase',
},
},
});
Using global transformers as an array.
dbs.defineModel({
name: 'user',
properties: {
firstName: {
type: DataType.STRING,
transform: [
'trim',
'capitalize',
],
},
},
});
Using global transformers with arguments.
dbs.defineModel({
name: 'article',
properties: {
annotation: {
type: DataType.STRING,
transform: {
truncate: 200,
capitalize: {firstWordOnly: true},
},
},
},
});
Global transformers without parameters can accept any arguments.
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,
},
},
},
});
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.
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.
dbs.defineModel({
name: 'comment',
properties: {
text: {
type: DataType.STRING,
transform: 'stripTags',
},
},
});
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.
// 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.
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.
dbs.defineModel({
name: 'article',
properties: {
slug: {
type: DataType.STRING,
transform: (value) => {
if (typeof value !== 'string') return value;
return value.toLowerCase().replace(/\s+/g, '-');
},
},
},
});
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, {} |
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 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:
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.
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.
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)
creates a new document;replaceById(id, data, filter = undefined)
completely replaces a document;replaceOrCreate(data, filter = undefined)
replaces or creates a new document;patchById(id, data, filter = undefined)
partially updates a document;patch(data, where = undefined)
updates all or matching documents;find(filter = undefined)
finds all or matching documents;findOne(filter = undefined)
finds the first matching document;findById(id, filter = undefined)
finds a document by its ID;delete(where = undefined)
deletes all or matching documents;deleteById(id)
deletes a document by its ID;exists(id) checks for
existence by ID;count(where = undefined)
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)filter: object query parameters (see 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, as the repository interacts directly
with the database through the adapter specified in the datasource.
// 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.
Creates a new document in the collection based on the provided data. Returns the created document with an assigned identifier.
Signature:
create(
data: WithOptionalId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
Examples
Create a new document.
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.
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.
// 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'}
// }
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:
replaceById(
id: IdType,
data: WithoutId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
Examples
Replace a document by its identifier.
// 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
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:
replaceOrCreate(
data: WithOptionalId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
Examples
Create a new document if id: 3 does not exist.
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.
const updatedProduct = await productRep.replaceOrCreate({
id: 1,
name: 'Laptop Pro',
price: 1500,
});
console.log(updatedProduct);
// {
// id: 1,
// name: 'Laptop Pro',
// price: 1500,
// }
Partially updates an existing document by its identifier, modifying only the provided fields. The other fields of the document remain unchanged.
Signature:
patchById(
id: IdType,
data: PartialWithoutId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
Examples
Partially update a document by its identifier.
// 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
// }
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:
patch(
data: PartialWithoutId<FlatData, IdName>,
where?: WhereClause<FlatData>,
): Promise<number>;
Examples
Update documents based on a condition.
// updates all products with a price less than 30
const updatedCount = await productRep.patch(
{inStock: false},
{price: {lt: 30}},
);
Update all documents.
// adds or updates the updatedAt field for all documents
const totalCount = await productRep.patch({
updatedAt: new Date(),
});
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:
find(filter?: FilterClause<FlatData>): Promise<FlatData[]>;
Examples
Find all documents.
const allProducts = await productRep.find();
Find documents by a where condition.
const cheapProducts = await productRep.find({
where: {price: {lt: 100}},
});
Find with sorting and limiting the result set.
const latestProducts = await productRep.find({
order: 'createdAt DESC',
limit: 10,
});
Finds the first document that matches the filter conditions. Returns
undefined if no documents are found.
Signature:
findOne(
filter?: FilterClause<FlatData>,
): Promise<FlatData | undefined>;
Examples
Find a single document by a condition.
const expensiveProduct = await productRep.findOne({
where: {price: {gt: 1000}},
order: 'price DESC',
});
Handling the case where a document is not found.
const product = await productRep.findOne({
where: {name: 'Non-existent Product'},
});
if (!product) {
console.log('Product not found.');
}
Finds a single document by its unique identifier. Throws an error if the document is not found.
Signature:
findById(
id: IdType,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
Examples
Find a document by id.
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.
const product = await productRep.findById(1, {
include: 'category',
});
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:
delete(where?: WhereClause<FlatData>): Promise<number>;
Examples
Delete documents based on a condition.
const deletedCount = await productRep.delete({
inStock: false,
});
Delete all documents.
const totalCount = await productRep.delete();
Deletes a single document by its unique identifier. Returns
true if the document was found and deleted, otherwise
false.
Signature:
deleteById(id: IdType): Promise<boolean>;
Examples
Delete a document by id.
const wasDeleted = await productRep.deleteById(1);
if (wasDeleted) {
console.log('The document was deleted.');
} else {
console.log('No document found to delete.');
}
Checks for the existence of a document with the specified identifier.
Returns true if the document exists, otherwise
false.
Signature:
exists(id: IdType): Promise<boolean>;
Examples
Check if a document exists by id.
const productExists = await productRep.exists(1);
if (productExists) {
console.log('A document with id 1 exists.');
}
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:
count(where?: WhereClause<FlatData>): Promise<number>;
Examples
Count documents based on a condition.
const cheapCount = await productRep.count({
price: {lt: 100},
});
Count all documents.
const totalCount = await productRep.count();
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
// 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'],
})
This parameter accepts an object with query conditions and supports the following set of comparison operators.
eq (strict
equality)neq (inequality)gt (greater than)lt (less than)gte
(greater than or equal to)lte (less than
or equal to)inq (in a list)nin (not in a
list)between (range)exists
(property existence)like (pattern
matching)nlike (excluding
pattern)ilike
(case-insensitive pattern)nilike
(case-insensitive excluding pattern)regexp
(regular expression)Conditions can be combined with logical operators:
Finds documents where the value of the specified property is exactly
equal to the provided value. This is a shorthand for the
{eq: ...} operator.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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).
// 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;// 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).
// 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).
// 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).
// 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).
// 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.
// 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.
// 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.
// finds documents where name is 'James' OR age is greater than 30
const res = await rep.find({
where: {
or: [
{name: 'James'},
{age: {gt: 30}}
],
},
});
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";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.
const res = await rep.find({
order: 'createdAt',
});
Sort by the createdAt field in descending order.
const res = await rep.find({
order: 'createdAt DESC',
});
Sort by multiple properties in different directions.
const res = await rep.find({
order: [
'title',
'price ASC',
'featured DESC',
],
});
i. The ASC sort direction is optional.
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).
Examples
Include a relation by its name.
const res = await rep.find({
include: 'city',
});
Include nested relations.
const res = await rep.find({
include: {
city: 'country',
},
});
Include multiple relations using an array.
const res = await rep.find({
include: [
'city',
'address',
'employees'
],
});
Using filtering for included documents.
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 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' │
└────────────────────┘
// 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'
// }
// }
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.
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.
Belongs To The current model references
the target model by its ID. type: "belongsTo" or
type: RelationType.BELONGS_TO
Has One The inverse of
belongsTo for a "one-to-one" relationship.
type: "hasOne" or
type: RelationType.HAS_ONE
Has Many The inverse of
belongsTo for a "one-to-many" relationship.
type: "hasMany" or
type: RelationType.HAS_MANY
References Many The current model
references the target model via an array of IDs.
type: "referencesMany" or
type: RelationType.REFERENCES_MANY
Polymorphic versions:
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_TORelationType.HAS_ONERelationType.HAS_MANYRelationType.REFERENCES_MANYThe 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:
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:
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'
// }
// }
The inverse of belongsTo for a "one-to-one"
relationship.
Current (profile) Target (user)
┌─────────────────────────┐ ┌─────────────────────────┐
│ id: 5 <──────────────│───┐ │ id: 1 │
│ ... │ └───│── profileId: 5 │
└─────────────────────────┘ │ ... │
└─────────────────────────┘
Relation definition:
// 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
},
},
});
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:
// 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
},
},
});
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:
// 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
},
},
});
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:
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 identifier is in "referenceId"
polymorphic: true,
},
},
});
Relation definition with explicit properties:
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 identifier
discriminator: 'referenceType', // property for the target's name
},
},
});
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:
// 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:
// 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 identifier
discriminator: 'ownerType', // property in target for current's name
},
},
});
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:
// 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:
// 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 identifier
discriminator: 'referenceType', // property in target for current's name
},
},
});
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.
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.
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.
Getting a typed repository with a specified model interface.
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, which comes with a set of TypeScript decorators and additional tools for working in a TypeScript environment.
npm run test
MIT