Advertisement
Guest User

Untitled

a guest
Oct 10th, 2019
142
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.19 KB | None | 0 0
  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. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement