channelCategoriesByName.service.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { Service, Inject } from 'typedi';
  2. import { ChannelCategory } from '../channel-category/channel-category.model';
  3. import { ChannelCategoryService } from '../channel-category/channel-category.service';
  4. import { ChannelCategoryWhereInput, } from '../../../generated';
  5. import { InjectRepository } from 'typeorm-typedi-extensions';
  6. import { Repository, getConnection, EntityManager } from 'typeorm';
  7. import { ChannelCategoriesByNameFTSOutput } from './channelCategoriesByName.resolver';
  8. interface RawSQLResult {
  9. origin_table: string,
  10. id: string,
  11. rank: number,
  12. highlight: string
  13. }
  14. @Service('ChannelCategoriesByNameFTSService')
  15. export class ChannelCategoriesByNameFTSService {
  16. readonly channelCategoryRepository: Repository<ChannelCategory>;
  17. constructor(
  18. @InjectRepository(ChannelCategory) channelCategoryRepository: Repository<ChannelCategory>
  19. ,@Inject('ChannelCategoryService') public readonly channelCategoryService: ChannelCategoryService
  20. ) {
  21. this.channelCategoryRepository = channelCategoryRepository;
  22. }
  23. /**
  24. * It takes available where inputs for the full text search(fts), generates sql queries
  25. * to be run with fts query.
  26. * @param wheres WhereInput[]
  27. */
  28. private async processWheres(wheres: any[]): Promise<[string, any[], number]> {
  29. const services: any[] = [this.channelCategoryService, ]
  30. const repositories = [this.channelCategoryRepository, ]
  31. let [queries, parameters, parameterCounter]: [string, any[], number] = [``, [], 0];
  32. const generateSqlQuery = (table: string, where: string) =>
  33. `
  34. SELECT '${table}_' || id AS unique_id FROM "${table}" ` + where;
  35. wheres.map((w, index) => {
  36. if (w) {
  37. let WHERE = `WHERE `;
  38. // Combine queries
  39. if (queries !== ``) {
  40. queries = queries.concat(`
  41. UNION ALL`);
  42. }
  43. const qb = services[index].buildFindQuery(w as any, undefined, undefined, ['id']);
  44. // Add query parameters to the parameters list
  45. parameters.push(...qb.getQueryAndParameters()[1]);
  46. // Remove the last item which is "table_name"."deleted_at" IS NULL
  47. qb.expressionMap.wheres.pop();
  48. // Combine conditions
  49. qb.expressionMap.wheres.map((w: any, index: number) => {
  50. let c = ``;
  51. if (w.condition.includes(`IN (:...`)) {
  52. // IN condition parameters
  53. const params: any[] = qb.expressionMap.parameters[`param${index}`];
  54. // Do nothing when IN condition has an empty list of values
  55. if (params.length !== 0) {
  56. const paramsAsString = params
  57. .map((_: any) => {
  58. parameterCounter += 1;
  59. return `$${parameterCounter}`;
  60. })
  61. .join(`, `);
  62. c = w.condition.replace(`(:...param${index})`, `(${paramsAsString})`);
  63. }
  64. } else if (w.condition.includes(`->>`)) {
  65. parameterCounter += 1;
  66. const m = w.condition.match(/->>.*\s=\s:.*/g);
  67. if (m === null)
  68. throw Error(`Failed to construct where condition for json field: ${w.condition}`);
  69. c = w.condition.replace(/=\s:.*/g, `= $${parameterCounter}`);
  70. } else {
  71. parameterCounter += 1;
  72. c = w.condition.replace(`:param${index}`, `$${parameterCounter}`);
  73. }
  74. WHERE = WHERE.concat(c, ` `, w.type.toUpperCase(), ` `);
  75. });
  76. // Remove unnecessary AND at the end.
  77. WHERE = WHERE.slice(0, -4);
  78. // Add new query to queryString
  79. queries = queries.concat(generateSqlQuery(repositories[index].metadata.tableName, WHERE));
  80. }
  81. });
  82. queries = `
  83. WITH selected_ids AS (`.concat(
  84. queries,
  85. `
  86. )`
  87. );
  88. return [queries, parameters, parameterCounter];
  89. }
  90. async search(
  91. text: string,
  92. limit = 5,
  93. skip = 0,
  94. whereChannelCategory?: ChannelCategoryWhereInput,
  95. ): Promise<ChannelCategoriesByNameFTSOutput[]> {
  96. const wheres = [whereChannelCategory, ]
  97. let [queries, parameters, parameterCounter]: [string, any[], number] = [``, [], 0];
  98. if (wheres.some(f => f !== undefined)) {
  99. [queries, parameters, parameterCounter] = await this.processWheres(wheres);
  100. }
  101. parameters.push(...[text, limit, skip]);
  102. return getConnection().transaction<ChannelCategoriesByNameFTSOutput[]>(
  103. 'REPEATABLE READ',
  104. async (em: EntityManager) => {
  105. const query = `
  106. ${queries}
  107. SELECT origin_table, id,
  108. ts_rank(tsv, phraseto_tsquery('english', $${parameterCounter + 1})) as rank,
  109. ts_headline(document, phraseto_tsquery('english', $${parameterCounter +
  110. 1})) as highlight
  111. FROM channel_categories_by_name_view
  112. WHERE phraseto_tsquery('english', $${parameterCounter + 1}) @@ tsv
  113. ${
  114. queries !== ``
  115. ? `AND unique_id IN (SELECT unique_id FROM selected_ids)`
  116. : ``
  117. }
  118. ORDER BY rank DESC
  119. LIMIT $${parameterCounter + 2}
  120. OFFSET $${parameterCounter + 3}`;
  121. const results = (await em.query(query, parameters)) as RawSQLResult[];
  122. if (results.length === 0) {
  123. return [];
  124. }
  125. const idMap:{ [id:string]: RawSQLResult } = {};
  126. results.forEach(item => idMap[item.id] = item);
  127. const ids: string[] = results.map(item => item.id);
  128. const channelCategorys: ChannelCategory[] = await em.createQueryBuilder<ChannelCategory>(ChannelCategory, 'ChannelCategory')
  129. .where("id IN (:...ids)", { ids }).getMany();
  130. const enhancedEntities = [...channelCategorys ].map((e) => {
  131. return { item: e,
  132. rank: idMap[e.id].rank,
  133. highlight: idMap[e.id].highlight,
  134. isTypeOf: idMap[e.id].origin_table } as ChannelCategoriesByNameFTSOutput;
  135. });
  136. return enhancedEntities.reduce((accum: ChannelCategoriesByNameFTSOutput[], entity) => {
  137. if (entity.rank > 0) {
  138. accum.push(entity);
  139. }
  140. return accum;
  141. }, []).sort((a,b) => b.rank - a.rank);
  142. })
  143. }
  144. }