import bodybuilder from 'bodybuilder' ; import DataLoader from 'dataloader' ; import _ from 'lodash' ; import { ARTICLE_STATE , DEFAULT_TAKE_PER_PAGE , MATERIALIZED_VIEW , TAG_ACTION , VIEW , } from 'common/enums' ; import { ServerError } from 'common/errors' ; import logger from 'common/logger' ; import { BaseService } from 'connectors' ; import { ItemData } from 'definitions' ; export class TagService extends BaseService { constructor ( ) { super ( 'tag' ) ; this . dataloader = new DataLoader ( this . baseFindByIds ) ; } /**
     * Find tags
     */ find = async ( { skip , take , sort , } ) => { let query = null ; const sortCreatedAt = ( by ) => this . knex . select ( ) . from ( this . table ) . orderBy ( 'created_at' , by ) ; if ( sort === 'hottest' ) { query = this . knex . select ( 'tag_id' , 'tag.*' ) . from ( 'article_tag' ) . count ( 'tag_id' ) . join ( 'tag' , 'tag_id' , 'tag.id' ) . groupBy ( 'tag_id' , 'tag.id' ) . orderBy ( 'count' , 'desc' ) ; } else if ( sort === 'oldest' ) { query = sortCreatedAt ( 'asc' ) ; } else if ( sort === 'newest' ) { query = sortCreatedAt ( 'desc' ) ; } else { query = sortCreatedAt ( 'desc' ) ; } if ( skip ) { query . offset ( skip ) ; } if ( take || take === 0 ) { query . limit ( take ) ; } return query ; } ; /**
     * Find tags by a given content.
     *
     */ findByContent = async ( { content } ) => this . knex . select ( ) . from ( this . table ) . where ( { content } ) ; findByContentIn = async ( contentIn ) => this . knex . select ( ) . from ( this . table ) . whereIn ( 'content' , contentIn ) ; /**
     * Find tags by a given article id.
     *
     */ findByArticleId = async ( { articleId } ) => this . knex . select ( 'tag.*' ) . from ( 'article_tag' ) . join ( this . table , 'tag.id' , 'article_tag.tag_id' ) . where ( { articleId } ) ; /**
     *  Find tags by a given creator id (user).
     */ findByCreator = async ( userId ) => { const query = this . knex . select ( ) . from ( this . table ) . where ( { creator : userId } ) . orderBy ( 'id' , 'desc' ) ; return query ; } ; /**
     *  Find tags by a given editor id (user).
     */ findByEditor = async ( userId ) => { const query = this . knex . select ( ) . from ( this . table ) . where ( this . knex . raw ( `editors @> ARRAY['${ userId }']` ) ) . orderBy ( 'id' , 'desc' ) ; return query ; } ; /**
     * Find tags by a given owner id (user).
     */ findByOwner = async ( userId ) => this . knex . select ( ) . from ( this . table ) . where ( { owner : userId } ) . orderBy ( 'id' , 'desc' ) ; /**
     * Find tags by a given maintainer id (user).
     *
     */ findByMaintainer = async ( userId ) => this . knex . select ( ) . from ( this . table ) . where ( { owner : userId } ) . orWhere ( this . knex . raw ( `editors @> ARRAY['${ userId }']` ) ) . orderBy ( 'id' , 'desc' ) ; /**
     * Create a tag, but return one if it's existing.
     *
     */ create = async ( { content , cover , creator , description , editors , owner , } ) => { const item = await this . knex ( this . table ) . select ( ) . where ( { content } ) . first ( ) ; if ( ! item ) { const tag = await this . baseCreate ( { content , cover , creator , description , editors , owner } , this . table ) ; await this . addToSearch ( { id : tag . id , content : tag . content , description : tag . description , } ) ; return tag ; } return item ; } ; /**
     * Count of a tag's participants.
     *
     */ countParticipants = async ( { id , exclude , } ) => { const subquery = this . knex . raw ( `(
        SELECT
            at.*, article.author_id
        FROM
            article_tag AS at
        INNER JOIN
            article ON article.id = at.article_id
        WHERE
            at.tag_id = ${ id }
    ) AS base` ) ; const result = await this . knex . from ( ( knex ) => { const source = knex . select ( 'author_id' ) . from ( subquery ) . groupBy ( 'author_id' ) ; if ( exclude ) { source . whereNotIn ( 'author_id' , exclude ) ; } source . as ( 'source' ) ; } ) . count ( ) . first ( ) ; return parseInt ( result ? ( result . count as string ) : '0' , 10 ) ; } ; /**
     * Find a tag's participants.
     *
     */ findParticipants = async ( { id , skip , take , exclude , } ) => { const subquery = this . knex . raw ( `(
        SELECT
            at.*, article.author_id
        FROM
            article_tag AS at
        INNER JOIN
            article ON article.id = at.article_id
        WHERE
            at.tag_id = ${ id }
        ORDER BY
            at.created_at
    ) AS base` ) ; const query = this . knex . select ( 'author_id' ) . from ( subquery ) . groupBy ( 'author_id' ) ; if ( exclude ) { query . whereNotIn ( 'author_id' , exclude ) ; } if ( take || take === 0 ) { query . limit ( take ) ; } if ( skip ) { query . offset ( skip ) ; } return query ; } ; /*********************************
     *                               *
     *             Follow            *
     *                               *
     *********************************/ follow = async ( { targetId , userId , } ) => { const data = { userId , targetId , action : TAG_ACTION . follow , } ; return this . baseUpdateOrCreate ( { where : data , data : { updatedAt : new Date ( ) , ... data } , table : 'action_tag' , } ) ; } ; unfollow = async ( { targetId , userId , } ) => this . knex . from ( 'action_tag' ) . where ( { targetId , userId , action : TAG_ACTION . follow , } ) . del ( ) ; /**
     * Find followers of a tag using id as pagination index.
     *
     */ findFollowers = async ( { targetId , skip , take , } ) => { const query = this . knex . select ( ) . from ( 'action_tag' ) . where ( { targetId , action : TAG_ACTION . follow } ) . orderBy ( 'id' , 'desc' ) ; if ( skip ) { query . andWhere ( 'id' , '<' , skip ) ; } if ( take || take === 0 ) { query . limit ( take ) ; } return query ; } ; /**
     * Determine if an user followed a tag or not by a given id.
     *
     */ isFollower = async ( { userId , targetId , } ) => { const result = await this . knex . select ( ) . from ( 'action_tag' ) . where ( { userId , targetId , action : TAG_ACTION . follow } ) . first ( ) ; return ! ! result ; } ; countFollowers = async ( targetId ) => { const result = await this . knex ( 'action_tag' ) . where ( { targetId , action : TAG_ACTION . follow } ) . count ( ) . first ( ) ; return parseInt ( result ? ( result . count as string ) : '0' , 10 ) ; } ; /*********************************
     *                               *
     *           Search              *
     *                               *
     *********************************/ initSearch = async ( ) => { const tags = await this . knex ( this . table ) . select ( 'id' , 'content' , 'description' ) ; return this . es . indexManyItems ( { index : this . table , items : tags . map ( ( tag ) => ( { ... tag , } ) ) , } ) ; } ; addToSearch = async ( { id , content , description , } ) => { try { return await this . es . indexItems ( { index : this . table , items : [ { id , content , description , } , ] , } ) ; } catch ( error ) { logger . error ( error ) ; } } ; updateSearch = async ( { id , content , description , } ) => { try { const result = await this . es . client . update ( { index : this . table , id , body : { doc : { content , description } , } , } ) ; return result ; } catch ( error ) { logger . error ( error ) ; } } ; search = async ( { key , take , skip , } ) => { const body = bodybuilder ( ) . query ( 'match' , 'content' , key ) . from ( skip ) . size ( take ) . build ( ) ; try { const result = await this . es . client . search ( { index : this . table , body , } ) ; const { hits } = result . body ; const ids = hits . hits . map ( ( { _id } ) => _id ) ; const tags = await this . baseFindByIds ( ids , this . table ) ; return { nodes : tags , totalCount : hits . total . value } ; } catch ( err ) { logger . error ( err ) ; throw new ServerError ( 'tag search failed' ) ; } } ; /*********************************
     *                               *
     *           Recommand           *
     *                               *
     *********************************/ findBoost = async ( tagId ) => { const tagBoost = await this . knex ( 'tag_boost' ) . select ( ) . where ( { tagId } ) . first ( ) ; if ( ! tagBoost ) { return 1 ; } return tagBoost . boost ; } ; setBoost = async ( { id , boost } ) => this . baseUpdateOrCreate ( { where : { tagId : id } , data : { tagId : id , boost , updatedAt : new Date ( ) } , table : 'tag_boost' , } ) ; findScore = async ( tagId ) => { const tag = await this . knex ( 'tag_count_view' ) . select ( ) . where ( { id : tagId } ) . first ( ) ; return tag . tagScore || 0 ; } ; /**
     * Find curation-like tags.
     *
     */ findCurationTags = ( { mattyId , fields = [ '*' ] , take , } ) => { const query = this . knex . select ( fields ) . from ( MATERIALIZED_VIEW . curation_tag_materialized ) . orderBy ( 'uuid' ) ; if ( take || take === 0 ) { query . limit ( take ) ; } return query ; } ; /**
     * Find non-curation-like tags based on score order.
     *
     */ findNonCurationTags = ( { mattyId , fields = [ '*' ] , oss = false , } ) => { const curation = this . findCurationTags ( { mattyId , fields : [ 'id' ] } ) ; const query = this . knex . select ( fields ) . from ( ( knex ) => { const source = knex . select ( ) . from ( oss ? VIEW . tag_count_view : MATERIALIZED_VIEW . tag_count_materialized ) . whereNotIn ( 'id' , curation ) . orderByRaw ( 'tag_score DESC NULLS LAST' ) . orderBy ( 'count' , 'desc' ) ; source . as ( 'source' ) ; } ) ; return query ; } ; /**
     * Find curation-like and non-curation-like tags in specific order.
     *
     */ findArrangedTags = async ( { mattyId , take , skip , oss = false , } ) => { const curation = this . findCurationTags ( { mattyId , fields : [ 'id' , this . knex . raw ( '1 as type' ) ] , } ) ; const nonCuration = this . findNonCurationTags ( { mattyId , fields : [ 'id' , this . knex . raw ( '2 as type' ) ] , oss , } ) ; const query = this . knex . select ( [ 'id' , 'type' ] ) . from ( curation . as ( 'curation' ) ) . unionAll ( [ nonCuration ] ) . orderBy ( 'type' ) ; if ( skip ) { query . offset ( skip ) ; } if ( take || take === 0 ) { query . limit ( take ) ; } return query ; } ; /**
     *
     * query, add and remove tag recommendation
     */ selected = async ( { take , skip } ) => { const query = this . knex ( 'tag' ) . select ( 'tag.*' , 'c.updated_at as chose_at' ) . join ( 'matters_choice_tag as c' , 'c.tag_id' , 'tag.id' ) . orderBy ( 'chose_at' , 'desc' ) ; if ( skip ) { query . offset ( skip ) ; } if ( take || take === 0 ) { query . limit ( take ) ; } return query ; } ; countSelectedTags = async ( ) => { const result = await this . knex ( 'matters_choice_tag' ) . count ( ) . first ( ) ; return parseInt ( result ? ( result . count as string ) : '0' , 10 ) ; } ; addTagRecommendation = ( tagId ) => this . baseFindOrCreate ( { where : { tagId } , data : { tagId } , table : 'matters_choice_tag' , } ) ; removeTagRecommendation = ( tagId ) => this . knex ( 'matters_choice_tag' ) . where ( { tagId } ) . del ( ) ; /*********************************
     *                               *
     *            Article            *
     *                               *
     *********************************/ createArticleTags = async ( { articleIds , creator , tagIds , selected , } ) => { articleIds = _ . uniq ( articleIds ) ; tagIds = _ . uniq ( tagIds ) ; const items = _ . flatten ( articleIds . map ( ( articleId ) => { return tagIds . map ( ( tagId ) => ( { articleId , creator , tagId , ... ( selected === true ? { selected } : { } ) , } ) ) ; } ) ) ; return this . baseBatchCreate ( items , 'article_tag' ) ; } ; /**
     * Update article tag.
     */ putArticleTag = async ( { articleId , tagId , data , } ) => this . knex ( 'article_tag' ) . where ( { articleId , tagId } ) . update ( data ) . returning ( '*' ) ; /**
     * Count tags by a given tag text.
     */ countArticles = async ( { id , selected , } ) => { const result = await this . knex ( 'article_tag' ) . join ( 'article' , 'article_id' , 'article.id' ) . countDistinct ( 'article_id' ) . where ( { tagId : id , state : ARTICLE_STATE . active , ... ( selected === true ? { selected } : { } ) , } ) . first ( ) ; return parseInt ( result ? ( result . count as string ) : '0' , 10 ) ; } ; /**
     * Find article ids by tag id with offset/limit
     */ findArticleIds = async ( { id : tagId , skip , take , selected , } ) => { const query = this . knex . select ( 'article_id' ) . from ( 'article_tag' ) . join ( 'article' , 'article_id' , 'article.id' ) . where ( { tagId , state : ARTICLE_STATE . active , ... ( selected === true ? { selected } : { } ) , } ) . orderBy ( 'article_tag.id' , 'desc' ) ; if ( skip ) { query . offset ( skip ) ; } if ( take || take === 0 ) { query . limit ( take ) ; } const result = await query ; return result . map ( ( { articleId } ) => articleId ) ; } ; /**
     * Find article ids by tag ids
     */ findArticleIdsByTagIds = async ( tagIds ) => { const result = await this . knex . select ( 'article_id' ) . from ( 'article_tag' ) . whereIn ( 'tag_id' , tagIds ) . groupBy ( 'article_id' ) ; return result . map ( ( { articleId } ) => articleId ) ; } ; deleteArticleTagsByArticleIds = async ( { articleIds , tagId , } ) => this . knex ( 'article_tag' ) . whereIn ( 'article_id' , articleIds ) . andWhere ( { tagId } ) . del ( ) ; deleteArticleTagsByTagIds = async ( { articleId , tagIds , } ) => this . knex ( 'article_tag' ) . whereIn ( 'tag_id' , tagIds ) . andWhere ( { articleId } ) . del ( ) ; isArticleSelected = async ( { articleId , tagId , } ) => { const result = await this . knex ( 'article_tag' ) . where ( { articleId , tagId , selected : true , } ) ; return result . length > 0 ; } ; /**
     * Find article covers by tag id.
     */ findArticleCovers = async ( { id } ) => this . knex . select ( 'article.cover' ) . from ( 'article_tag' ) . join ( 'article' , 'article_id' , 'article.id' ) . whereNotNull ( 'cover' ) . andWhere ( { tagId : id , state : ARTICLE_STATE . active , } ) . limit ( DEFAULT_TAKE_PER_PAGE ) . orderBy ( 'article_tag.id' , 'asc' ) ; /*********************************
     *                               *
     *              OSS              *
     *                               *
     *********************************/ renameTag = async ( { tagId , content } ) => this . baseUpdate ( tagId , { content , updatedAt : new Date ( ) } ) ; mergeTags = async ( { tagIds , content , creator , editors , owner , } ) => { const newTag = await this . create ( { content , creator , editors , owner } ) ; await this . addToSearch ( { id : newTag . id , content : newTag . content , description : newTag . description , } ) ; const articleIds = await this . findArticleIdsByTagIds ( tagIds ) ; await this . createArticleTags ( { articleIds , creator , tagIds : [ newTag . id ] } ) ; await this . knex ( 'article_tag' ) . whereIn ( 'tag_id' , tagIds ) . del ( ) ; await this . knex . raw ( `
      INSERT INTO action_tag (user_id, action, target_id)
      SELECT
        user_id, 'follow', ${ newTag . id }
      FROM action_tag
      WHERE target_id in (${ tagIds . join ( ',' ) })
      GROUP BY user_id
    ` ) ; await this . knex ( 'action_tag' ) . whereIn ( 'target_id' , tagIds ) . andWhere ( 'action' , 'follow' ) . del ( ) ; await this . baseBatchDelete ( tagIds ) ; return newTag ; } ; followTags = async ( userId , tags ) => { const items = await this . findByContentIn ( tags ) ; await Promise . all ( items . filter ( Boolean ) . map ( ( tag ) => this . follow ( { targetId : tag . id , userId } ) . then ( ( err ) => console . error ( new Date ( ) , `follow ${ tag } failed:` , err ) ) ) ) ; } ; }