SHARE
TWEET

Untitled

a guest Oct 10th, 2019 101 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import { Brackets, EntityRepository, Repository, SelectQueryBuilder } from 'typeorm';
  2.  
  3. import { COLLATE, QUESTS_TREND_INTERVAL } from '../../common/constants';
  4. import {
  5.     Duration, QuestRunStatus, QuestStatus,
  6.     RouteType, SearchMode, SortOrder
  7. } from '../../common/enums';
  8. import { SearchQuery } from '../../dto/params/search-query';
  9. import { QuestEntity } from '../entities/quest.entity';
  10. import { UserEntity } from '../entities/user.entity';
  11.  
  12. export interface StatResult {
  13.     status: QuestStatus;
  14.     count: string;
  15. }
  16.  
  17. export interface QuestRunCountResult {
  18.     id: number;
  19.     count: string;
  20. }
  21.  
  22. @EntityRepository(QuestEntity)
  23. export class QuestsRepository extends Repository<QuestEntity> {
  24.     public getQuest(questId: number, user: UserEntity): Promise<QuestEntity> {
  25.         return this
  26.             .createQueryBuilder('quest')
  27.             .innerJoinAndSelect('quest.user', 'user')
  28.             .leftJoinAndSelect('quest.routeByType', 'route')
  29.             .leftJoinAndSelect('quest.counters', 'counter')
  30.             .leftJoinAndSelect('quest.media', 'media')
  31.             .leftJoinAndMapOne('quest.fan', 'quest.favouredBy', 'fan', 'fan.id = :userId', {userId: user ? user.id : null})
  32.             .where('quest.id = :questId', {questId})
  33.             .getOne();
  34.     }
  35.  
  36.     /**
  37.      * Ищет квесты в соответствии с параметрами.
  38.      * @param query параметры фильтрации/пагинации
  39.      * @param userId ID текущего пользователя
  40.      * @param authorId ID пользователя - автора квестов
  41.      */
  42.     public searchQuests(query: SearchQuery, userId?: number, authorId?: number): Promise<[QuestEntity[], number]> {
  43.         const builder = this
  44.             .createQueryBuilder('quest')
  45.             .innerJoinAndSelect('quest.user', 'user')
  46.             .leftJoinAndSelect('quest.counters', 'counter')
  47.             .leftJoinAndSelect('quest.media', 'media')
  48.             .addSelect(qb => qb
  49.                     .select('rating_counter.value / runs_counter.value')
  50.                     .from('quest_counter', 'rating_counter')
  51.                     .innerJoin('quest_counter', 'runs_counter', 'rating_counter.quest_id = runs_counter.quest_id')
  52.                     .where('quest.id = rating_counter.quest_id')
  53.                     .andWhere(`rating_counter.code = 'QUEST_RATING'`)
  54.                     .andWhere(`runs_counter.code = 'QUEST_RUNS'`),
  55.                 'rating'
  56.             )
  57.             .where('quest.status = :status', {status: query.status || QuestStatus.ACTIVE});
  58.  
  59.         if (userId) {
  60.             builder.leftJoinAndMapOne('quest.fan', 'quest.favouredBy', 'fan', 'fan.id = :userId', {userId});
  61.         }
  62.  
  63.         this.buildSearchFilter(builder, query, userId, authorId);
  64.         this.buildSearchOrder(builder, query);
  65.  
  66.         if ([
  67.             SearchMode.TREND,
  68.             SearchMode.RECOMMENDED,
  69.             SearchMode.SUBSCRIPTIONS
  70.         ].indexOf(query.searchMode) === -1 || query.full) {
  71.             // builder.skip(query.offset).take(query.limit);
  72.         }
  73.  
  74.         return builder.getManyAndCount();
  75.     }
  76.  
  77.     /**
  78.      * Возвращает статистику по квестам пользователя,
  79.      * отфильтрованным в соответствии с параметрами.
  80.      * @param query параметры фильтрации
  81.      * @param userId ID пользователя
  82.      */
  83.     public getQuestStats(query: SearchQuery, userId: number): Promise<StatResult[]> {
  84.         const builder = this.createQueryBuilder('quest');
  85.         this.buildSearchFilter(builder, query, userId);
  86.         return builder
  87.             .select('quest.status', 'status')
  88.             .addSelect('COUNT(DISTINCT quest.id)', 'count')
  89.             .groupBy('quest.status')
  90.             .getRawMany();
  91.     }
  92.  
  93.     /**
  94.      * Возвращает количество прохождений квестов за последний месяц.
  95.      * TODO: выпилить с обновлением typeorm до 0.3.0.
  96.      * @param questIds идентификаторы квестов
  97.      */
  98.     public getQuestRunCountLastMonth(questIds: number[]): Promise<QuestRunCountResult[]> {
  99.         const builder = this
  100.             .createQueryBuilder('quest')
  101.             .select('quest.id', 'id')
  102.             .whereInIds(questIds);
  103.         this.selectQuestRunCountLastMonth(builder, 'count');
  104.         return builder.getRawMany();
  105.     }
  106.  
  107.     private buildSearchFilter(
  108.         builder: SelectQueryBuilder<QuestEntity>,
  109.         query: SearchQuery,
  110.         userId?: number,
  111.         authorId?: number
  112.     ): void {
  113.         const {searchString} = query;
  114.         if (searchString) {
  115.             let str = searchString.trim().toLowerCase();
  116.             if (str.length > 2) {
  117.                 const fts = str.split(/\s+/).map(str => `${str}:*`).join(' | ');
  118.                 // quest_search_index - это специальная табличка, связанная 1:1 с quest и
  119.                 // содержащая индекс (наименование, описания, ключевые слова) для полнотекстового поиска
  120.                 builder
  121.                     .innerJoin('quest_search_index', 'qsi', 'qsi.quest_id = quest.id')
  122.                     .andWhere(`TO_TSQUERY('russian', :fts) @@ qsi.search_index`, {fts});
  123.             } else if (str.length > 0) {
  124.                 str = `%${str}%`;
  125.                 builder.andWhere(new Brackets(qb => qb
  126.                     .where(`LOWER(quest.name COLLATE ${COLLATE}) LIKE :str`, {str})
  127.                     .orWhere(`LOWER(quest.short_description COLLATE ${COLLATE}) LIKE :str`, {str})
  128.                     .orWhere(`LOWER(quest.full_description COLLATE ${COLLATE}) LIKE :str`, {str})
  129.                     .orWhere(`LOWER(ARRAY_TO_STRING(quest.keywords, ' ') COLLATE ${COLLATE}) LIKE :str`, {str})
  130.                 ));
  131.             }
  132.         }
  133.  
  134.         const {category} = query;
  135.         if (category) {
  136.             builder.andWhere(':category = ANY(quest.categories)', {category});
  137.         }
  138.  
  139.         const {location} = query;
  140.         if (location) {
  141.             builder.andWhere(':location = ANY(quest.regions)', {location});
  142.         }
  143.  
  144.         // делаем выборку независимо от того, используется ли в фильтре
  145.         builder.leftJoinAndSelect('quest.routeByType', 'route');
  146.  
  147.         const {routeType} = query;
  148.         // игнорируем параметры routeLengthFrom, routeLengthTo и duration до
  149.         // решения вопроса с длиной и продолжительностью маршрутов типа BOAT
  150.         if (routeType === RouteType.BOAT) {
  151.             builder.andWhere(':routeType = ANY(quest.routeTypes)', {routeType});
  152.         } else {
  153.             const {routeLengthFrom, routeLengthTo, duration} = query;
  154.             if (routeLengthFrom) {
  155.                 builder.andWhere('route.length >= :routeLengthFrom', {routeLengthFrom});
  156.             }
  157.             if (routeLengthTo) {
  158.                 builder.andWhere('route.length <= :routeLengthTo', {routeLengthTo});
  159.             }
  160.             if (duration === Duration.SHORT) {
  161.                 builder.andWhere('route.duration < 2');
  162.             } else if (duration === Duration.MIDDLE) {
  163.                 builder.andWhere('route.duration BETWEEN 2 AND 4');
  164.             } else if (duration === Duration.LONG) {
  165.                 builder.andWhere('route.duration BETWEEN 4 AND 8');
  166.             } else if (duration === Duration.VERYLONG) {
  167.                 builder.andWhere('route.duration > 8');
  168.             }
  169.             if (routeType) {
  170.                 builder.andWhere('route.routeType = :routeType', {routeType});
  171.             }
  172.         }
  173.  
  174.         if (!userId && query.searchMode !== SearchMode.PROFILE) return;
  175.  
  176.         switch (query.searchMode) {
  177.             case SearchMode.TREND: {
  178.                 if (!query.full) {
  179.                     builder.andWhere('quest.popular = TRUE');
  180.                 }
  181.                 break;
  182.             }
  183.             case SearchMode.RECOMMENDED: {
  184.                 // этот запрос написан от безысходности, в попытке выразить требование {
  185.                 //   есть прохождение у участника, у которого в списке пройденных есть хотя бы
  186.                 //   один квест, который прошел текущий пользователь ИЛИ квест создан автором,
  187.                 //   квесты которого уже проходил текущий пользователь
  188.                 // }
  189.                 // вероятно, требование можно переформулировать, а запрос - упростить, либо иначе
  190.                 // решить задачу рекомендаций (cron tasks? машинное обучение?)
  191.                 builder.andWhere(new Brackets(qb => qb
  192.                     .where('quest.recommend = TRUE')
  193.                     .orWhere(`
  194.                         quest.id IN (
  195.                             SELECT quest_id
  196.                             FROM quest_run
  197.                             WHERE quest_id IN (
  198.                                 SELECT quest_id
  199.                                 FROM quest_run
  200.                                 WHERE user_account_id = :userId
  201.                                 AND status = :runStatus
  202.                             )
  203.                             AND user_account_id != :userId
  204.                             AND status = :runStatus
  205.                         )
  206.                     `, {
  207.                         userId,
  208.                         runStatus: QuestRunStatus.FINISHED
  209.                     })
  210.                     .orWhere(`
  211.                         quest.author_id IN (
  212.                             SELECT author_id
  213.                             FROM quest
  214.                             INNER JOIN quest_run
  215.                                 ON quest.id = quest_run.quest_id
  216.                                 AND quest_run.user_account_id = :userId
  217.                             WHERE author_id != :userId
  218.                             AND quest_run.status = :runStatus
  219.                         )
  220.                     `, {
  221.                         userId,
  222.                         runStatus: QuestRunStatus.FINISHED
  223.                     })
  224.                 ));
  225.                 break;
  226.             }
  227.             case SearchMode.SUBSCRIPTIONS: {
  228.                 // этот запрос так же уныл, как и предыдущий
  229.                 builder.andWhere(new Brackets(qb => qb
  230.                     .orWhere(`
  231.                         quest.author_id IN (
  232.                             SELECT user_id
  233.                             FROM user_follower
  234.                             WHERE follower_id = :userId
  235.                         )
  236.                     `, {userId})
  237.                     .orWhere(`
  238.                         quest.id IN (
  239.                             SELECT quest_id
  240.                             FROM quest_run
  241.                             WHERE status = :runStatus
  242.                             AND user_account_id IN (
  243.                                 SELECT user_id
  244.                                 FROM user_follower
  245.                                 WHERE follower_id = :userId
  246.                             )
  247.                         )
  248.                     `, {
  249.                         userId,
  250.                         runStatus: QuestRunStatus.FINISHED
  251.                     })
  252.                 ));
  253.                 break;
  254.             }
  255.             case SearchMode.FAVOURITES: {
  256.                 builder
  257.                     .innerJoin('user_quest', 'uq', 'uq.quest_id = quest.id')
  258.                     .andWhere('uq.user_account_id = :userId', {userId});
  259.                 break;
  260.             }
  261.             case SearchMode.PROFILE: {
  262.                 authorId = authorId || userId;
  263.                 builder.andWhere('quest.author_id = :authorId', {authorId});
  264.                 break;
  265.             }
  266.         }
  267.     }
  268.  
  269.     private async buildSearchOrder(
  270.         builder: SelectQueryBuilder<QuestEntity>,
  271.         query: SearchQuery
  272.     ): any {
  273.         const defaultOrder = query.sort === SortOrder.DATE ?
  274.             'quest.creationDate' : 'rating';
  275.         switch (query.searchMode) {
  276.             case SearchMode.TREND:
  277.             case SearchMode.RECOMMENDED:
  278.             case SearchMode.SUBSCRIPTIONS: {
  279.                 if (query.full && !query.sort) {
  280.                     this.selectQuestRunCountLastMonth(builder);
  281.                     builder.orderBy('runs_month', 'DESC', 'NULLS LAST');
  282.                 } else {
  283.                     builder.orderBy(defaultOrder, 'DESC', 'NULLS LAST');
  284.                 }
  285.                 break;
  286.             }
  287.             case SearchMode.FAVOURITES: {
  288.                 builder.orderBy(defaultOrder, 'DESC', 'NULLS LAST');
  289.                 break;
  290.             }
  291.             case SearchMode.PROFILE: {
  292.                 builder.orderBy('quest.updateDate', 'DESC', 'NULLS LAST');
  293.                 break;
  294.             }
  295.             default: {
  296.                 builder.orderBy(defaultOrder, 'DESC', 'NULLS LAST');
  297.             }
  298.         }
  299.  
  300.         if (query.searchMode === SearchMode.TREND) {
  301.             builder.addSelect(`CASE
  302.                         WHEN "quest"."id" = 30 THEN 0
  303.                         ELSE 1
  304.                     END`, 'incurr').orderBy('incurr', 'ASC')
  305.  
  306.             const r = await builder.getOne();
  307.             console.log(r);
  308.         }
  309.     }
  310.  
  311.     private selectQuestRunCountLastMonth(builder: SelectQueryBuilder<QuestEntity>, alias = 'runs_month'): void {
  312.         // количество прохождений за последний месяц
  313.         builder.addSelect(qb => qb
  314.                 .select('COUNT(id)')
  315.                 .from('quest_run', 'quest_run')
  316.                 .where('quest.id = quest_run.quest_id')
  317.                 .andWhere(`quest_run.startDate + INTERVAL '${QUESTS_TREND_INTERVAL}' > NOW()`),
  318.             alias
  319.         );
  320.     }
  321.  
  322.     public getLastDraftInEdit(user: UserEntity): Promise<QuestEntity> {
  323.         return this
  324.             .createQueryBuilder('quest')
  325.             .where('quest.author_id = :userId', {userId: user.id})
  326.             .andWhere('quest.status = :status', {status: QuestStatus.DRAFT})
  327.             .orderBy('quest.updateDate', 'DESC')
  328.             .limit(1)
  329.             .getOne();
  330.     }
  331.  
  332.     public suggestKeywords(searchString: string, limit: number): Promise<{ kw: string }[]> {
  333.         const str = searchString.trim().toLowerCase();
  334.         return this.query(`
  335.             WITH keywords AS (
  336.                 SELECT DISTINCT UNNEST(keywords) AS kw
  337.                 FROM quest
  338.             )
  339.             SELECT kw, POSITION($1 IN LOWER(kw COLLATE ${COLLATE})) AS pos
  340.             FROM keywords
  341.             WHERE LOWER(kw COLLATE ${COLLATE}) LIKE $2
  342.             ORDER BY pos, kw
  343.             LIMIT $3
  344.         `, [str, `%${str}%`, limit]);
  345.     }
  346.  
  347.     public cleanFavourite(quest: QuestEntity): Promise<void> {
  348.         return this.query('DELETE FROM user_quest WHERE quest_id = $1', [quest.id]);
  349.     }
  350. }
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top