Як провести пагінізацію з Мангустом у Node.js?


231

Я пишу веб-сторінку з Node.js та мангустом. Як я можу запам'ятати результати, отримані від .find()дзвінка? Я хотів би функціонал, порівнянний з "LIMIT 50,100"SQL.


Використовуйте властивості пропуску та обмеження, знаходячи дані зі збору.
Арун Сахані

Це посилання може бути використане для вас детально. laxmanchavda.blogspot.com/2018/06/…
laxman

Відповіді:


278

Я дуже розчарований прийнятими відповідями в цьому питанні. Це не буде масштабуватися. Якщо ви читаєте тонкий шрифт на cursor.skip ():

Метод cursor.skip () часто дорогий, оскільки він вимагає, щоб сервер пройшов з початку колекції або індексу, щоб отримати зміщення або пропуск позиції, перш ніж почати повертати результат. У міру збільшення зміщення (наприклад, сторінкиNumber вище), cursor.skip () стане повільнішим і більш інтенсивним процесором. У більших колекціях cursor.skip () може стати пов'язаним IO.

Щоб домогтися розбиття сторінок масштабованим способом, поєднайте ліміт () разом з принаймні одним критерієм фільтра, дата створеногоOn відповідає багатьом цілям.

MyModel.find( { createdOn: { $lte: request.createdOnBefore } } )
.limit( 10 )
.sort( '-createdOn' )

105
Але як би ви отримали другу сторінку з цього запиту без пропуску? Якщо ви переглядаєте 10 результатів на сторінці, а є 100 результатів, як ви потім визначите значення зміщення чи пропуск? Ви не відповідаєте на питання про пагинацію, тому не можете бути "розчаровані", хоча це дійсна обережність. Хоча ця сама проблема є в зміщенні MySQL, обмежте. Потрібно пройти дерево до заліку перед поверненням результатів. Я б взяв це з зерном солі, якщо ваші набори результатів менші за 1 мільйон, і показника продуктивності не можна зберегти, скористайтеся skip ().
Лекс

13
Я нобій, коли мова йде про мангуста / монгоб, але щоб відповісти на запитання Лекса, виявляється, що, оскільки результати впорядковані ' -createdOn', ви заміните значення request.createdOnBeforeна найменше значення, що createdOnповертається в попередньому наборі результатів, а потім запит.
Terry Lewis

9
@JoeFrambach Запит на основі createOn видається проблематичним. Пропуск був вбудований не просто так. Документи є лише попередженням про ефективність перемикання циклу через індекс btree, що стосується всіх СУБД. Для користувачів питання "щось порівнянне MySQL з обмеженням 50 100" .skip цілком правильно.
Лекс

8
Хоча цікаво, проблема з цією відповіддю, як зазначає коментар @Lex, полягає в тому, що ви можете пропустити лише "вперед" або "назад" через результати - у вас немає "сторінок", на які можна перейти (наприклад, Сторінка 1, Сторінка 2 , Сторінка 3), не роблячи декількох послідовних запитів, щоб визначити, з чого почати пагинацію, що, як я підозрюю, у більшості випадків буде повільніше, ніж просто пропуск. Звичайно, можливо, вам не потрібно буде додавати можливість пропускати певні сторінки.
Ієн Коллінз

20
Ця відповідь містить цікаві моменти, але вона не відповідає оригінальному заданому питанню.
пару

227

Після детального ознайомлення з API Mongoose з інформацією, наданою Родольфом, я зрозумів це рішення:

MyModel.find(query, fields, { skip: 10, limit: 5 }, function(err, results) { ... });

21
А як щодо "рахувати"? Вам потрібно це знати, скільки сторінок існує.
Олексій Саатчі

36
Не масштабує.
Кріс Хінкл

4
Пояснення Кріса Гінкла, чому це не масштабується: stackoverflow.com/a/23640287/165330 .
imme

7
@ChrisHinkle Це, мабуть, так і у всіх СУБД. Коментар Лекса нижче пов'язаної відповіді, здається, пояснює більше.
csotiriou

2
@Avij так. Я використовував для цього ідентифікатор. що ви там робите - це відправити останній ідентифікатор записів назад на сервер і отримати деякі записи з id, більшими за надіслані. Як ідентифікується Id, це буде набагато швидше
Джордж Бейлі

108

Пагинація за допомогою мангуста, експресу та нефриту - ось посилання на мій блог з більш детальною інформацією

var perPage = 10
  , page = Math.max(0, req.param('page'))

Event.find()
    .select('name')
    .limit(perPage)
    .skip(perPage * page)
    .sort({
        name: 'asc'
    })
    .exec(function(err, events) {
        Event.count().exec(function(err, count) {
            res.render('events', {
                events: events,
                page: page,
                pages: count / perPage
            })
        })
    })

26
Дякуємо, що опублікували свою відповідь! Будь ласка, уважно прочитайте FAQ щодо самореклами . Також зауважте, що Ви зобов’язуєтесь публікувати відмову щоразу, коли Ви посилаєтесь на власний сайт / продукт.
Ендрю Барбер

Math.max(0, undefined)повернеться undefined, Це працювало для мене:let limit = Math.abs(req.query.limit) || 10; let page = (Math.abs(req.query.page) || 1) - 1; Schema.find().limit(limit).skip(limit * page)
Monfa.red

55

Ви можете ланцюг просто так:

var query = Model.find().sort('mykey', 1).skip(2).limit(5)

Виконайте запит за допомогою exec

query.exec(callback);

Дякую за вашу відповідь, як додається до цього зворотний дзвінок з результатом?
Томас

2
execFind (функція (... наприклад: var page = req.param('p'); var per_page = 10; if (page == null) { page = 0; } Location.count({}, function(err, count) { Location.find({}).skip(page*per_page).limit(per_page).execFind(function(err, locations) { res.render('index', { locations: locations }); }); });
todd

9
зауважте: це не буде працювати в мангусті , але воно буде працювати в mongodb-native-driver.
Джессі

39

У цьому випадку ви можете додати запит pageта / або limitдо своєї URL-адреси у вигляді рядка запиту.

Наприклад:
?page=0&limit=25 // this would be added onto your URL: http:localhost:5000?page=0&limit=25

Оскільки це було б Stringнам, потрібно перетворити його на а Numberдля наших розрахунків. Давайте зробимо це за допомогою parseIntметоду і надамо також деякі значення за замовчуванням.

const pageOptions = {
    page: parseInt(req.query.page, 10) || 0,
    limit: parseInt(req.query.limit, 10) || 10
}

sexyModel.find()
    .skip(pageOptions.page * pageOptions.limit)
    .limit(pageOptions.limit)
    .exec(function (err, doc) {
        if(err) { res.status(500).json(err); return; };
        res.status(200).json(doc);
    });

BTW Пагинація починається з0


5
будь ласка, додайте {{page: parseInt (req.query.page) || 0, ...} до параметра.
imalik8088

@ imalik8088 Дякую, проте параметри строки AFAIK обробляються автоматично mongoose.
CENT1PEDE

1
Очікував поведінку, але в моєму випадку вона не змогла
приховати

@ imalik8088 Це дивно. Можливо, якщо ви могли показати помилку відтворення, я можу відредагувати свою відповідь. Дякую.
CENT1PEDE

2
Чи змусить це мангуст знаходити кожен запис, перш ніж застосовувати умови?
FluffyBeing

37

Ви можете використовувати невеликий пакет під назвою Mongoose Paginate, що робить його простіше.

$ npm install mongoose-paginate

Після в своїх маршрутах чи контролері просто додайте:

/**
 * querying for `all` {} items in `MyModel`
 * paginating by second page, 10 items per page (10 results, page 2)
 **/

MyModel.paginate({}, 2, 10, function(error, pageCount, paginatedResults) {
  if (error) {
    console.error(error);
  } else {
    console.log('Pages:', pageCount);
    console.log(paginatedResults);
  }
}

2
Це оптимізовано?
Аргенто

16

Це зразковий приклад, ви можете спробувати це,

var _pageNumber = 2,
  _pageSize = 50;

Student.count({},function(err,count){
  Student.find({}, null, {
    sort: {
      Name: 1
    }
  }).skip(_pageNumber > 0 ? ((_pageNumber - 1) * _pageSize) : 0).limit(_pageSize).exec(function(err, docs) {
    if (err)
      res.json(err);
    else
      res.json({
        "TotalCount": count,
        "_Array": docs
      });
  });
 });

11

Спробуйте використовувати мангусту функцію для пагинації. Ліміт - це кількість записів на сторінку та номер сторінки.

var limit = parseInt(body.limit);
var skip = (parseInt(body.page)-1) * parseInt(limit);

 db.Rankings.find({})
            .sort('-id')
            .limit(limit)
            .skip(skip)
            .exec(function(err,wins){
 });

10

Це те, що я зробив це за кодом

var paginate = 20;
var page = pageNumber;
MySchema.find({}).sort('mykey', 1).skip((pageNumber-1)*paginate).limit(paginate)
    .exec(function(err, result) {
        // Write some stuff here
    });

Саме так я це зробив.


1
Як отримати загальну кількість сторінок
Rhushikesh

Привіт @Rhushikesh, Ви можете використовувати функцію count (), щоб отримати кількість. Але, мабуть, потрібен ще один запит із бази даних. Детальніше тут mongoosejs.com/docs/api.html#model_Model.count
Індра Сантоса

@Rhushikesh отримайте підрахунок і розділіть його на межу
edthethird

count()застаріло. використанняcountDocuments()
Руслан

7

Запит;
пошук = Ім'я продукту,

Парами;
сторінка = 1

// Pagination
router.get("/search/:page", (req, res, next) => {
  const resultsPerPage = 5;
  const page = req.params.page >= 1 ? req.params.page : 1;
  const query = req.query.search;

  Product.find({ name: query })
    .select("name")
    .sort({ name: "asc" })
    .limit(resultsPerPage)
    .skip(resultsPerPage * page)
    .then((results) => {
      return res.status(200).send(results);
    })
    .catch((err) => {
      return res.status(500).send(err);
    });
});

Дякую за цю відповідь; Я спробував це спочатку, прочитавши нитку, бо це був один із останніх. Однак, коли я його реалізував, я виявив помилку - як це написано зараз, вона ніколи не поверне першу сторінку результатів, оскільки вона ЗАВЖДИ матиме пропускне значення. Спробуйте додати "page = page-1" перед викликом Product.find ().
Interog

6

Ось версія, яку я додаю до всіх своїх моделей. Це залежить від підкреслення для зручності та асинхронізації для продуктивності. Опція дозволяє вибирати та сортувати поле за допомогою синтаксису мангуста.

var _ = require('underscore');
var async = require('async');

function findPaginated(filter, opts, cb) {
  var defaults = {skip : 0, limit : 10};
  opts = _.extend({}, defaults, opts);

  filter = _.extend({}, filter);

  var cntQry = this.find(filter);
  var qry = this.find(filter);

  if (opts.sort) {
    qry = qry.sort(opts.sort);
  }
  if (opts.fields) {
    qry = qry.select(opts.fields);
  }

  qry = qry.limit(opts.limit).skip(opts.skip);

  async.parallel(
    [
      function (cb) {
        cntQry.count(cb);
      },
      function (cb) {
        qry.exec(cb);
      }
    ],
    function (err, results) {
      if (err) return cb(err);
      var count = 0, ret = [];

      _.each(results, function (r) {
        if (typeof(r) == 'number') {
          count = r;
        } else if (typeof(r) != 'number') {
          ret = r;
        }
      });

      cb(null, {totalCount : count, results : ret});
    }
  );

  return qry;
}

Приєднайте його до вашої схеми моделі.

MySchema.statics.findPaginated = findPaginated;

5

Вище відповіді - це добре.

Просто доповнення для всіх, хто перебуває в асинхрії - чекайте, а не обіцяйте !!

const findAllFoo = async (req, resp, next) => {
    const pageSize = 10;
    const currentPage = 1;

    try {
        const foos = await FooModel.find() // find all documents
            .skip(pageSize * (currentPage - 1)) // we will not retrieve all records, but will skip first 'n' records
            .limit(pageSize); // will limit/restrict the number of records to display

        const numberOfFoos = await FooModel.countDocuments(); // count the number of records for that model

        resp.setHeader('max-records', numberOfFoos);
        resp.status(200).json(foos);

    } catch (err) {
        resp.status(500).json({
            message: err
        });
    }
};

5

Просте і потужне рішення для сторінки

async getNextDocs(no_of_docs_required: number, last_doc_id?: string) {
    let docs

    if (!last_doc_id) {
        // get first 5 docs
        docs = await MySchema.find().sort({ _id: -1 }).limit(no_of_docs_required)
    }
    else {
        // get next 5 docs according to that last document id
        docs = await MySchema.find({_id: {$lt: last_doc_id}})
                                    .sort({ _id: -1 }).limit(no_of_docs_required)
    }
    return docs
}

last_doc_id: останній ідентифікатор документа, який ви отримали

no_of_docs_required: кількість документів, які ви хочете отримати, тобто 5, 10, 50 і т.д.

  1. Якщо ви не надаєте last_doc_id метод, ви отримаєте 5 найновіших документів
  2. Якщо ви надали, last_doc_idтоді ви отримаєте наступні, тобто 5 документів.

4

Ви також можете використовувати наступний рядок коду

per_page = parseInt(req.query.per_page) || 10
page_no = parseInt(req.query.page_no) || 1
var pagination = {
  limit: per_page ,
  skip:per_page * (page_no - 1)
}
users = await User.find({<CONDITION>}).limit(pagination.limit).skip(pagination.skip).exec()

цей код буде працювати в останній версії mongo


3

Грунтовним підходом до реалізації цього було б передача значень із фронтену за допомогою рядка запиту . Скажімо, ми хочемо отримати сторінку №2, а також обмежити вихід на 25 результатів .
Рядок запиту виглядатиме так:?page=2&limit=25 // this would be added onto your URL: http:localhost:5000?page=2&limit=25

Давайте подивимось код:

// We would receive the values with req.query.<<valueName>>  => e.g. req.query.page
// Since it would be a String we need to convert it to a Number in order to do our
// necessary calculations. Let's do it using the parseInt() method and let's also provide some default values:

  const page = parseInt(req.query.page, 10) || 1; // getting the 'page' value
  const limit = parseInt(req.query.limit, 10) || 25; // getting the 'limit' value
  const startIndex = (page - 1) * limit; // this is how we would calculate the start index aka the SKIP value
  const endIndex = page * limit; // this is how we would calculate the end index

// We also need the 'total' and we can get it easily using the Mongoose built-in **countDocuments** method
  const total = await <<modelName>>.countDocuments();

// skip() will return a certain number of results after a certain number of documents.
// limit() is used to specify the maximum number of results to be returned.

// Let's assume that both are set (if that's not the case, the default value will be used for)

  query = query.skip(startIndex).limit(limit);

  // Executing the query
  const results = await query;

  // Pagination result 
 // Let's now prepare an object for the frontend
  const pagination = {};

// If the endIndex is smaller than the total number of documents, we have a next page
  if (endIndex < total) {
    pagination.next = {
      page: page + 1,
      limit
    };
  }

// If the startIndex is greater than 0, we have a previous page
  if (startIndex > 0) {
    pagination.prev = {
      page: page - 1,
      limit
    };
  }

 // Implementing some final touches and making a successful response (Express.js)

const advancedResults = {
    success: true,
    count: results.length,
    pagination,
    data: results
 }
// That's it. All we have to do now is send the `results` to the frontend.
 res.status(200).json(advancedResults);

Я б запропонував застосувати цю логіку до середнього програмного забезпечення, щоб ви могли використовувати її для різних маршрутів / контролерів.


2

Найпростіший і швидший спосіб - це сторінка з прикладом objectId;

Початковий стан навантаження

condition = {limit:12, type:""};

Візьміть перший і останній ObjectId з даних відповідей

Сторінка наступна умова

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c662d", lastId:"57762a4c875adce3c38c6615"};

Сторінка наступна умова

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c6645", lastId:"57762a4c875adce3c38c6675"};

У мангуста

var condition = {};
    var sort = { _id: 1 };
    if (req.body.type == "next") {
        condition._id = { $gt: req.body.lastId };
    } else if (req.body.type == "prev") {
        sort = { _id: -1 };
        condition._id = { $lt: req.body.firstId };
    }

var query = Model.find(condition, {}, { sort: sort }).limit(req.body.limit);

query.exec(function(err, properties) {
        return res.json({ "result": result);
});

2

Найкращий підхід (IMO) - використовувати пропуск та обмеження АЛЕ в межах обмежених колекцій чи документів.

Для здійснення запиту в обмежених документах ми можемо використовувати специфічний індекс, наприклад індекс у полі типу DATE. Дивіться це нижче

let page = ctx.request.body.page || 1
let size = ctx.request.body.size || 10
let DATE_FROM = ctx.request.body.date_from
let DATE_TO = ctx.request.body.date_to

var start = (parseInt(page) - 1) * parseInt(size)

let result = await Model.find({ created_at: { $lte: DATE_FROM, $gte: DATE_TO } })
    .sort({ _id: -1 })
    .select('<fields>')
    .skip( start )
    .limit( size )        
    .exec(callback)

2

Найпростіший плагін для сторінки.

https://www.npmjs.com/package/mongoose-paginate-v2

Додайте плагін до схеми, а потім використовуйте метод пагіната моделі:

var mongoose         = require('mongoose');
var mongoosePaginate = require('mongoose-paginate-v2');

var mySchema = new mongoose.Schema({ 
    /* your schema definition */ 
});

mySchema.plugin(mongoosePaginate);

var myModel = mongoose.model('SampleModel',  mySchema); 

myModel.paginate().then({}) // Usage

цей плагін розбитий мангустом v5.5.5
Ісаак Пак

1

Це приклад функції для отримання результату моделі навичок з використанням пагінацій та лімітів

 export function get_skills(req, res){
     console.log('get_skills');
     var page = req.body.page; // 1 or 2
     var size = req.body.size; // 5 or 10 per page
     var query = {};
     if(page < 0 || page === 0)
     {
        result = {'status': 401,'message':'invalid page number,should start with 1'};
        return res.json(result);
     }
     query.skip = size * (page - 1)
     query.limit = size
     Skills.count({},function(err1,tot_count){ //to get the total count of skills
      if(err1)
      {
         res.json({
            status: 401,
            message:'something went wrong!',
            err: err,
         })
      }
      else 
      {
         Skills.find({},{},query).sort({'name':1}).exec(function(err,skill_doc){
             if(!err)
             {
                 res.json({
                     status: 200,
                     message:'Skills list',
                     data: data,
                     tot_count: tot_count,
                 })
             }
             else
             {
                 res.json({
                      status: 401,
                      message: 'something went wrong',
                      err: err
                 })
             }
        }) //Skills.find end
    }
 });//Skills.count end

}


0

Ви можете написати запит так.

mySchema.find().skip((page-1)*per_page).limit(per_page).exec(function(err, articles) {
        if (err) {
            return res.status(400).send({
                message: err
            });
        } else {
            res.json(articles);
        }
    });

сторінка: номер сторінки, що надходить від клієнта як параметри запиту.
per_page: на сторінці немає результатів

Якщо ви використовуєте стек MEAN, наступна публікація в блозі надає велику частину інформації для створення сторінки на передньому кінці за допомогою кутового завантажувального інтерфейсу з використанням кутового інтерфейсу та з використанням мангустних методів пропуску та обмеження в бекенді.

дивіться: https://techpituwa.wordpress.com/2015/06/06/mean-js-pagination-with-angular-ui-bootstrap/


0

Ви можете використовувати skip () та limit (), але це дуже неефективно. Кращим рішенням буде сортування в індексованому полі плюс limit (). Ми з Wunderflats опублікували тут невеличку бібліотеку: https://github.com/wunderflats/goosepage У ній використовується перший спосіб.


0

Якщо ви використовуєте мангусту як джерело для спокійного api, перегляньте " restify-mungoose " та його запити. У ній вбудований саме такий функціонал.

Будь-який запит у колекції надає тут корисні заголовки

test-01:~$ curl -s -D - localhost:3330/data?sort=-created -o /dev/null
HTTP/1.1 200 OK
link: </data?sort=-created&p=0>; rel="first", </data?sort=-created&p=1>; rel="next", </data?sort=-created&p=134715>; rel="last"
.....
Response-Time: 37

Таким чином, ви отримуєте загальний сервер з відносно лінійним часом завантаження для запитів до колекцій. Це дивовижно і на що слід звернути увагу, якщо ви хочете зайнятися власною реалізацією.


0
app.get("/:page",(req,res)=>{
        post.find({}).then((data)=>{
            let per_page = 5;
            let num_page = Number(req.params.page);
            let max_pages = Math.ceil(data.length/per_page);
            if(num_page == 0 || num_page > max_pages){
                res.render('404');
            }else{
                let starting = per_page*(num_page-1)
                let ending = per_page+starting
                res.render('posts', {posts:data.slice(starting,ending), pages: max_pages, current_page: num_page});
            }
        });
});

0
**//localhost:3000/asanas/?pageNo=1&size=3**

//requiring asanas model
const asanas = require("../models/asanas");


const fetchAllAsanasDao = () => {
    return new Promise((resolve, reject) => {

    var pageNo = parseInt(req.query.pageNo);
    var size = parseInt(req.query.size);
    var query = {};
        if (pageNo < 0 || pageNo === 0) {
            response = {
                "error": true,
                "message": "invalid page number, should start with 1"
            };
            return res.json(response);
        }
        query.skip = size * (pageNo - 1);
        query.limit = size;

  asanas
            .find(pageNo , size , query)
        .then((asanasResult) => {
                resolve(asanasResult);
            })
            .catch((error) => {
                reject(error);
            });

    });
}

0

Використовуйте цей простий плагін.

https://github.com/WebGangster/mongoose-paginate-v2

Установка

npm install mongoose-paginate-v2
Використання Додати плагін до схеми, а потім скористатися методом пагіната моделі:

const mongoose         = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');

const mySchema = new mongoose.Schema({ 
  /* your schema definition */ 
});

mySchema.plugin(mongoosePaginate);

const myModel = mongoose.model('SampleModel',  mySchema); 

myModel.paginate().then({}) // Usage


Цей плагін був "запропонований" вже в іншій відповіді. Було б також корисно знати, що ви, якщо є учасником цього пакету.
lukas_o

@lukas_o Так. Я творець плагіна.
Aravind NC

0

згідно з

Кріс Хінкл

відповідь:

//assume every page has 50 result
const results = (req.query.page * 1) * 50;
MyModel.find( { fieldNumber: { $lte: results} })
.limit( 50 )
.sort( '+fieldNumber' )

//one thing left is create a fieldNumber on the schema thas holds ducument number

0

Використання ts-мангусти-пагінації

    const trainers = await Trainer.paginate(
        { user: req.userId },
        {
            perPage: 3,
            page: 1,
            select: '-password, -createdAt -updatedAt -__v',
            sort: { createdAt: -1 },
        }
    )

    return res.status(200).json(trainers)

0
let page,limit,skip,lastPage, query;
 page = req.params.page *1 || 1;  //This is the page,fetch from the server
 limit = req.params.limit * 1 || 1; //  This is the limit ,it also fetch from the server
 skip = (page - 1) * limit;   // Number of skip document
 lastPage = page * limit;   //last index 
 counts = await userModel.countDocuments() //Number of document in the collection

query = query.skip(skip).limit(limit) //current page

const paginate = {}

//For previous page
if(skip > 0) {
   paginate.prev = {
       page: page - 1,
       limit: limit
} 
//For next page
 if(lastPage < counts) {
  paginate.next = {
     page: page + 1,
     limit: limit
}
results = await query //Here is the final results of the query.

-1

Вдалося досягти результатів і за допомогою асинхронізації / очікування.

Приклад наведеного нижче коду, використовуючи обробник асинхронізації з hapi v17 та мангустом v5

{
            method: 'GET',
            path: '/api/v1/paintings',
            config: {
                description: 'Get all the paintings',
                tags: ['api', 'v1', 'all paintings']
            },
            handler: async (request, reply) => {
                /*
                 * Grab the querystring parameters
                 * page and limit to handle our pagination
                */
                var pageOptions = {
                    page: parseInt(request.query.page) - 1 || 0, 
                    limit: parseInt(request.query.limit) || 10
                }
                /*
                 * Apply our sort and limit
                */
               try {
                    return await Painting.find()
                        .sort({dateCreated: 1, dateModified: -1})
                        .skip(pageOptions.page * pageOptions.limit)
                        .limit(pageOptions.limit)
                        .exec();
               } catch(err) {
                   return err;
               }

            }
        }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.