diff --git a/db/migrations/20241112112619_create_tag_stats_view.js b/db/migrations/20241112112619_create_tag_stats_view.js new file mode 100644 index 000000000..8e6fc2752 --- /dev/null +++ b/db/migrations/20241112112619_create_tag_stats_view.js @@ -0,0 +1,58 @@ +const materialized = 'tag_stats_materialized' + +exports.up = async (knex) => { + await knex.raw(`DROP MATERIALIZED VIEW IF EXISTS ${materialized}`) + + await knex.raw(` + CREATE MATERIALIZED VIEW ${materialized} AS + WITH article_tags AS ( + SELECT + at.tag_id, + at.article_id, + article.author_id + FROM article_tag at + INNER JOIN article + ON article.id = at.article_id + INNER JOIN "user" u + ON u.id = article.author_id + WHERE article.state = 'active' + AND u.state NOT IN ('forzen', 'archived') + AND u.id NOT IN ( + SELECT user_id + FROM user_restriction + ) + AND at.created_at AT TIME ZONE 'Asia/Taipei' >= NOW() - INTERVAL '12 months' + AND at.created_at AT TIME ZONE 'Asia/Taipei' < NOW() + ), + tag_stats AS ( + SELECT + tag_id, + COUNT(article_id)::INT AS all_articles, + COUNT(DISTINCT author_id)::INT AS all_users + FROM article_tags + GROUP BY tag_id + ), + user_threshold AS ( + SELECT DISTINCT + PERCENTILE_CONT(0.15) WITHIN GROUP (ORDER BY all_users) AS threshold + FROM tag_stats + ) + SELECT + ts.tag_id, + tag.content, + ts.all_articles, + ts.all_users + FROM tag_stats ts + INNER JOIN tag + ON tag.id = ts.tag_id + CROSS JOIN user_threshold ut + WHERE ts.all_users > ut.threshold + ORDER BY + ts.all_users DESC, + ts.all_articles DESC + `) +} + +exports.down = async (knex) => { + await knex.raw(`DROP MATERIALIZED VIEW IF EXISTS ${materialized}`) +} diff --git a/db/migrations/20241112165500_create_article_stats_view.js b/db/migrations/20241112165500_create_article_stats_view.js new file mode 100644 index 000000000..e6b257ff7 --- /dev/null +++ b/db/migrations/20241112165500_create_article_stats_view.js @@ -0,0 +1,27 @@ +const table = 'article_stats_materialized' + +exports.up = async (knex) => { + await knex.raw(` + CREATE MATERIALIZED VIEW ${table} AS + SELECT + COALESCE(r.article_id, c.reference_id) as article_id, + COALESCE(r.reads, 0) as reads, + COALESCE(c.claps, 0) as claps + FROM ( + SELECT article_id, sum(timed_count)::int AS reads + FROM article_read_count + WHERE user_id IS NOT NULL + GROUP BY article_id + ) r + FULL OUTER JOIN ( + SELECT reference_id, sum(amount)::int AS claps + FROM appreciation + WHERE purpose = 'appreciate' + GROUP BY reference_id + ) c ON r.article_id = c.reference_id + `) +} + +exports.down = async (knex) => { + await knex.raw(`DROP MATERIALIZED VIEW IF EXISTS ${table}`) +} diff --git a/db/migrations/20241113094622_create_tag_hottest_view.js b/db/migrations/20241113094622_create_tag_hottest_view.js new file mode 100644 index 000000000..490063fd0 --- /dev/null +++ b/db/migrations/20241113094622_create_tag_hottest_view.js @@ -0,0 +1,71 @@ +const table = 'tag_hottest_materialized' + +exports.up = async (knex) => { + await knex.raw(` + CREATE MATERIALIZED VIEW ${table} AS + WITH raw_article_data AS ( + SELECT + at.tag_id, + to_char(article.created_at, 'YYYYMM') AS month, + at.article_id, + article.author_id + FROM article_tag AS at + INNER JOIN article ON article.id = at.article_id + INNER JOIN "user" AS u ON u.id = article.author_id + WHERE article.state = 'active' + AND u.state NOT in('frozen', 'archived') + AND u.id NOT in(SELECT user_id FROM user_restriction) + ), + monthly_stats AS ( + SELECT tag_id, month, + count(article_id)::int articles, + count(DISTINCT author_id)::int users + FROM raw_article_data + GROUP BY tag_id, month + ), + tag_averages AS ( + SELECT tag_id, + count(month) AS months, + avg(articles) AS mean_articles, + avg(users) AS mean_users + FROM monthly_stats + GROUP BY tag_id + ), + tag_z_scores AS ( + SELECT tag_id, months, + (months - avg(months) OVER()) / NULLIF(stddev(months) OVER(), 0) AS z_months, + mean_articles, + (mean_articles - avg(mean_articles) OVER()) / NULLIF(stddev(mean_articles) OVER(), 0) AS z_articles, + mean_users, + (mean_users - avg(mean_users) OVER()) / NULLIF(stddev(mean_users) OVER(), 0) AS z_users + FROM tag_averages + ), + significant_scores AS ( + SELECT tag_id, months, + CASE WHEN z_months < 2 THEN 0 ELSE z_months END AS z_months, + mean_articles, + CASE WHEN z_articles < 2 THEN 0 ELSE z_articles END AS z_articles, + mean_users, + CASE WHEN z_users < 2 THEN 0 ELSE z_users END AS z_users + FROM tag_z_scores + ) + SELECT + base.tag_id, + tag.content, + base.months, + base.mean_articles, + base.mean_users, + base.score + FROM ( + SELECT *, z_months * z_articles * z_users AS score + FROM significant_scores + ) AS base + INNER JOIN tag ON tag.id = base.tag_id + WHERE base.score > 0 + ORDER BY base.score DESC + `) +} + +exports.down = async (knex) => { + await knex.raw(`DROP MATERIALIZED VIEW IF EXISTS ${table}`) +} diff --git a/schema.graphql b/schema.graphql index feba11be2..347ef8b68 100644 --- a/schema.graphql +++ b/schema.graphql @@ -43,8 +43,9 @@ type Mutation { """Edit an article.""" editArticle(input: EditArticleInput!): Article! - """Subscribe or Unsubscribe article""" - toggleSubscribeArticle(input: ToggleItemInput!): Article! + """Bookmark or unbookmark article""" + toggleSubscribeArticle(input: ToggleItemInput!): Article! @deprecated(reason: "Use toggleBookmarkArticle instead") + toggleBookmarkArticle(input: ToggleItemInput!): Article! """Appreciate an article.""" appreciateArticle(input: AppreciateArticleInput!): Article! @@ -52,27 +53,12 @@ type Mutation { """Read an article.""" readArticle(input: ReadArticleInput!): Article! - """Follow or unfollow tag.""" - toggleFollowTag(input: ToggleItemInput!): Tag! - - """Create or update tag.""" - putTag(input: PutTagInput!): Tag! - - """Update member, permission and othters of a tag.""" - updateTagSetting(input: UpdateTagSettingInput!): Tag! - - """Add one tag to articles.""" - addArticlesTags(input: AddArticlesTagsInput!): Tag! - - """Update articles' tag.""" - updateArticlesTags(input: UpdateArticlesTagsInput!): Tag! - - """Delete one tag from articles""" - deleteArticlesTags(input: DeleteArticlesTagsInput!): Tag! + """Bookmark or unbookmark tag.""" + toggleFollowTag(input: ToggleItemInput!): Tag! @deprecated(reason: "Use toggleBookmarkTag instead") + toggleBookmarkTag(input: ToggleItemInput!): Tag! toggleArticleRecommend(input: ToggleRecommendInput!): Article! updateArticleState(input: UpdateArticleStateInput!): Article! updateArticleSensitive(input: UpdateArticleSensitiveInput!): Article! - toggleTagRecommend(input: ToggleRecommendInput!): Tag! deleteTags(input: DeleteTagsInput!): Boolean renameTag(input: RenameTagInput!): Tag! mergeTags(input: MergeTagsInput!): Tag! @@ -168,9 +154,6 @@ type Mutation { """Reset user or payment password.""" resetPassword(input: ResetPasswordInput!): Boolean - """Change user email.""" - changeEmail(input: ChangeEmailInput!): User! @deprecated(reason: "use 'setEmail' instead") - """Set user email.""" setEmail(input: SetEmailInput!): User! @@ -180,11 +163,7 @@ type Mutation { """Set user currency preference.""" setCurrency(input: SetCurrencyInput!): User! - """Register user, can only be used on matters.{town,news} website.""" - userRegister(input: UserRegisterInput!): AuthResult! @deprecated(reason: "use 'emailLogin' instead") - """Login user.""" - userLogin(input: UserLoginInput!): AuthResult! @deprecated(reason: "use 'emailLogin' instead") emailLogin(input: EmailLoginInput!): AuthResult! """Get signing message.""" @@ -208,15 +187,9 @@ type Mutation { """Remove a social login from current user.""" removeSocialLogin(input: RemoveSocialLoginInput!): User! - """Reset crypto wallet.""" - resetWallet(input: ResetWalletInput!): User! @deprecated(reason: "use 'removeWalletLogin' instead") - """Logout user.""" userLogout: Boolean! - """Generate or claim a Liker ID through LikeCoin""" - generateLikerId: User! @deprecated(reason: "No longer in use") - """Reset Liker ID""" resetLikerId(input: ResetLikerIdInput!): User! @@ -392,9 +365,6 @@ type Article implements Node & PinnableWork { """Total number of readers of this article.""" readerCount: Int! - """Subscribers of this article.""" - subscribers(input: ConnectionArgs!): UserConnection! - """Limit the nuhmber of appreciate per user.""" appreciateLimit: Int! @@ -407,13 +377,13 @@ type Article implements Node & PinnableWork { """This value determines if current viewer can SuperLike or not.""" canSuperLike: Boolean! - """This value determines if current Viewer has subscribed of not.""" - subscribed: Boolean! + """This value determines if current Viewer has bookmarked of not.""" + subscribed: Boolean! @deprecated(reason: "Use bookmarked instead") + bookmarked: Boolean! """ This value determines if this article is an author selected article or not. """ - sticky: Boolean! @deprecated(reason: "Use pinned instead") pinned: Boolean! """Translation of article title and content.""" @@ -431,15 +401,6 @@ type Article implements Node & PinnableWork { """Cumulative reading time in seconds""" readTime: Float! - """Drafts linked to this article.""" - drafts: [Draft!] @deprecated(reason: "Use Article.newestUnpublishedDraft or Article.newestPublishedDraft instead") - - """Newest unpublished draft linked to this article.""" - newestUnpublishedDraft: Draft - - """Newest published draft linked to this article.""" - newestPublishedDraft: Draft! - """Revision Count""" revisionCount: Int! @@ -551,41 +512,17 @@ type Tag implements Node { articles(input: TagArticlesInput!): ArticleConnection! articlesExcludeSpam(input: TagArticlesInput!): ArticleConnection! - """This value determines if this article is selected by this tag or not.""" - selected(input: TagSelectedInput!): Boolean! - """Time of this tag was created.""" createdAt: DateTime! - """Tag's cover link.""" - cover: String - - """Description of this tag.""" - description: String - - """Editors of this tag.""" - editors(input: TagEditorsInput): [User!] - - """Creator of this tag.""" - creator: User - - """Owner of this tag.""" - owner: User - """This value determines if current viewer is following or not.""" isFollower: Boolean - """Followers of this tag.""" - followers(input: ConnectionArgs!): UserConnection! - - """Participants of this tag.""" - participants(input: ConnectionArgs!): UserConnection! - """Tags recommended based on relations to current tag.""" recommended(input: ConnectionArgs!): TagConnection! - """This value determines if it is official.""" - isOfficial: Boolean + """Authors recommended based on relations to current tag.""" + recommendedAuthors(input: ConnectionArgs!): UserConnection! """Counts of this tag.""" numArticles: Int! @@ -639,7 +576,6 @@ type ArticleTranslation { type TagOSS { boost: Float! score: Float! - selected: Boolean! } type ArticleConnection implements Connection { @@ -695,9 +631,6 @@ input PublishArticleInput { input EditArticleInput { id: ID! state: ArticleState - - """deprecated, use pinned instead""" - sticky: Boolean pinned: Boolean title: String summary: String @@ -772,36 +705,6 @@ input MergeTagsInput { content: String! } -input PutTagInput { - id: ID - content: String - cover: ID - description: String -} - -input UpdateTagSettingInput { - id: ID! - type: UpdateTagSettingType! - editors: [ID!] -} - -input AddArticlesTagsInput { - id: ID! - articles: [ID!] - selected: Boolean -} - -input UpdateArticlesTagsInput { - id: ID! - articles: [ID!] - isSelected: Boolean! -} - -input DeleteArticlesTagsInput { - id: ID! - articles: [ID!] -} - enum TagArticlesSortBy { byHottestDesc byCreatedAtDesc @@ -811,20 +714,9 @@ input TagArticlesInput { after: String first: Int oss: Boolean - selected: Boolean sortBy: TagArticlesSortBy = byCreatedAtDesc } -input TagSelectedInput { - id: ID - mediaHash: String -} - -input TagEditorsInput { - excludeAdmin: Boolean - excludeOwner: Boolean -} - input TransactionsReceivedByArgs { after: String first: Int @@ -874,14 +766,6 @@ enum RecommendTypes { search } -enum UpdateTagSettingType { - adopt - leave - add_editor - remove_editor - leave_editor -} - input CampaignInput { shortHash: String! } @@ -2580,9 +2464,6 @@ type Recommendation { """Activities based on user's following, sort by creation time.""" following(input: RecommendationFollowingInput!): FollowingActivityConnection! - """Articles recommended based on recently read article tags.""" - readTagsArticles(input: ConnectionArgs!): ArticleConnection! @deprecated(reason: "Merged into following") - """Global articles sort by publish time.""" newest(input: ConnectionArgs!): ArticleConnection! newestExcludeSpam(input: ConnectionArgs!): ArticleConnection! @@ -2802,9 +2683,6 @@ type Liker { """Total LIKE left in wallet.""" total: Float! - - """Rate of LikeCoin/USD""" - rateUSD: Float @deprecated(reason: "No longer in use") } type UserOSS { @@ -3097,13 +2975,6 @@ input ResetPasswordInput { type: ResetPasswordType } -input ChangeEmailInput { - oldEmail: String! - oldEmailCodeId: ID! - newEmail: String! - newEmailCodeId: ID! -} - input VerifyEmailInput { email: String! code: String! @@ -3113,21 +2984,6 @@ input SetCurrencyInput { currency: QuoteCurrency } -input UserRegisterInput { - email: String! - userName: String - displayName: String! - password: String! - description: String - codeId: ID! - referralCode: String -} - -input UserLoginInput { - email: String! - password: String! -} - input GenerateSigningMessageInput { address: String! purpose: SigningMessagePurpose @@ -3145,12 +3001,6 @@ input WalletLoginInput { """nonce from generateSigningMessage""" nonce: String! - """required for wallet register""" - email: String @deprecated(reason: "No longer in use") - - """email verification code, required for wallet register""" - codeId: ID @deprecated(reason: "No longer in use") - """used in register""" language: UserLanguage referralCode: String @@ -3160,10 +3010,6 @@ input ResetLikerIdInput { id: ID! } -input ResetWalletInput { - id: ID! -} - input UpdateNotificationSettingInput { type: NotificationSettingType! enabled: Boolean! @@ -3171,7 +3017,6 @@ input UpdateNotificationSettingInput { input UpdateUserInfoInput { displayName: String - userName: String @deprecated(reason: "use 'setUserName' instead") avatar: ID description: String language: UserLanguage @@ -3265,9 +3110,6 @@ enum VerificationCodeType { register email_verify email_otp - email_reset @deprecated(reason: "No longer in use") - email_reset_confirm @deprecated(reason: "No longer in use") - password_reset @deprecated(reason: "No longer in use") payment_password_reset } diff --git a/src/common/enums/table.ts b/src/common/enums/table.ts index 14ab5bbcd..f8569d9da 100644 --- a/src/common/enums/table.ts +++ b/src/common/enums/table.ts @@ -1,16 +1,16 @@ export enum VIEW { - tag_count_view = 'tag_count_view', user_reader_view = 'user_reader_view', article_count_view = 'article_count_view', article_hottest_view = 'article_hottest_view', transaction_delta_view = 'transaction_delta_view', article_value_view = 'article_value_view', - tags_lasts_view = 'mat_views.tags_lasts', } export enum MATERIALIZED_VIEW { - tag_count_materialized = 'tag_count_materialized', tags_lasts_view_materialized = 'mat_views.tags_lasts_view_materialized', + tag_stats_materialized = 'tag_stats_materialized', + tag_hottest_materialized = 'tag_hottest_materialized', + article_stats_materialized = 'article_stats_materialized', user_reader_materialized = 'user_reader_materialized', featured_comment_materialized = 'featured_comment_materialized', curation_tag_materialized = 'curation_tag_materialized', diff --git a/src/connectors/__test__/tagService.test.ts b/src/connectors/__test__/tagService.test.ts index 5932a9a65..0fecf2d51 100644 --- a/src/connectors/__test__/tagService.test.ts +++ b/src/connectors/__test__/tagService.test.ts @@ -123,23 +123,10 @@ describe('findArticleIds', () => { }) }) -test('findArticleCovers', async () => { - const covers = await tagService.findArticleCovers({ id: '2' }) - expect(covers).toBeDefined() - - const cached = await tagService.findArticleCovers({ id: '2' }) - expect(cached).toEqual(covers) -}) - test('create', async () => { const content = 'foo' const tag = await tagService.create( - { - content, - creator: '0', - editors: [], - owner: '0', - }, + { content, creator: '0' }, { columns: ['id', 'content'], } diff --git a/src/connectors/likecoin/index.ts b/src/connectors/likecoin/index.ts index 0f6e7f644..271fc0462 100644 --- a/src/connectors/likecoin/index.ts +++ b/src/connectors/likecoin/index.ts @@ -53,19 +53,6 @@ const ERROR_CODES = { INSUFFICIENT_PERMISSION: 'INSUFFICIENT_PERMISSION', } -type LikeCoinLocale = - | 'en' - | 'zh' - | 'cn' - | 'de' - | 'es' - | 'fr' - | 'it' - | 'ja' - | 'ko' - | 'pt' - | 'ru' - type RequestProps = { endpoint: string headers?: { [key: string]: any } @@ -235,78 +222,6 @@ export class LikeCoin { return data.access_token } - /** - * Register - */ - public check = async ({ user, email }: { user: string; email?: string }) => { - try { - const res = await this.request({ - endpoint: ENDPOINTS.check, - method: 'POST', - data: { - user, - email, - }, - }) - const data = _.get(res, 'data') - - if (data === 'OK') { - return user - } else { - throw res - } - } catch (e) { - const data = _.get(e, 'response.data') - const alternative = _.get(data, 'alternative') - - if (alternative) { - return alternative - } - - throw e - } - } - - public register = async ({ - user, - token, - displayName, - email, - locale = 'zh', - isEmailEnabled, - ip, - }: { - user: string - token: string - ip?: string - displayName?: string - email?: string - locale?: LikeCoinLocale - isEmailEnabled?: boolean - }) => { - const res = await this.request({ - endpoint: ENDPOINTS.register, - withClientCredential: true, - method: 'POST', - data: { - user, - token, - displayName, - email, - locale, - isEmailEnabled, - }, - ip, - }) - const data = _.get(res, 'data') - - if (data.accessToken && data.refreshToken) { - return data - } else { - throw res - } - } - /** * Claim, Transfer or Bind */ @@ -356,19 +271,6 @@ export class LikeCoin { return data.cosmosLIKE || data.walletLIKE } - public rate = async (currency: 'usd' | 'twd' = 'usd') => { - const res = await this.request({ - endpoint: ENDPOINTS.rate, - method: 'GET', - params: { - currency, - }, - }) - const price = _.get(res, 'data.price') - - return price - } - /** * Check if user is a civic liker */ diff --git a/src/connectors/queue/publication.ts b/src/connectors/queue/publication.ts index fd8759e61..68f328142 100644 --- a/src/connectors/queue/publication.ts +++ b/src/connectors/queue/publication.ts @@ -448,22 +448,12 @@ export class PublicationQueue { let tags = articleVersion.tags as string[] if (tags && tags.length > 0) { - // get tag editor - const tagEditors = environment.mattyId - ? [environment.mattyId, article.authorId] - : [article.authorId] - // create tag records, return tag record if already exists const dbTags = ( (await Promise.all( tags.filter(Boolean).map((content: string) => tagService.create( - { - content, - creator: article.authorId, - editors: tagEditors, - owner: article.authorId, - }, + { content, creator: article.authorId }, { columns: ['id', 'content'], skipCreate: normalizeTagInput(content) !== content, diff --git a/src/connectors/tagService.ts b/src/connectors/tagService.ts index 5e6a456b3..fe5f1621c 100644 --- a/src/connectors/tagService.ts +++ b/src/connectors/tagService.ts @@ -6,11 +6,8 @@ import { difference } from 'lodash' import { ARTICLE_STATE, MAX_TAGS_PER_ARTICLE_LIMIT, - DEFAULT_TAKE_PER_PAGE, TAG_ACTION, MATERIALIZED_VIEW, - CACHE_PREFIX, - CACHE_TTL, } from 'common/enums' import { environment } from 'common/environment' import { TooManyTagsForArticleError, ForbiddenError } from 'common/errors' @@ -20,7 +17,7 @@ import { normalizeTagInput, excludeSpam as excludeSpamModifier, } from 'common/utils' -import { BaseService, CacheService, SystemService } from 'connectors' +import { BaseService, SystemService } from 'connectors' const logger = getLogger('service-tag') @@ -100,54 +97,6 @@ export class TagService extends BaseService { // .join(this.table, 'tag.id', 'article_tag.tag_id') .whereIn('article_id', articleIds) - /** - * Find tags by a given creator id (user). - */ - public findByCreator = async (userId: string) => { - const query = this.knex - .select() - .from(this.table) - .where({ creator: userId }) - .orderBy('id', 'desc') - - return query - } - - /** - * Find tags by a given editor id (user). - */ - public findByEditor = async (userId: string) => { - 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). - */ - public findByOwner = async (userId: string) => - this.knex - .select() - .from(this.table) - .where({ owner: userId }) - .orderBy('id', 'desc') - - /** - * Find tags by a given maintainer id (user). - * - */ - public findByMaintainer = async (userId: string) => - this.knex - .select() - .from(this.table) - .where({ owner: userId }) - .orWhere(this.knex.raw(`editors @> ARRAY[?]`, [userId])) - .orderBy('id', 'desc') - public findByAuthorUsage = async ({ userId, skip, @@ -227,21 +176,7 @@ export class TagService extends BaseService { * this create may return null if skipCreate */ public create = async ( - { - content, - cover, - creator, - description, - editors, - owner, - }: { - content: string - cover?: string | null - creator: string - description?: string - editors: string[] - owner: string - }, + { content, creator }: { content: string; creator: string }, { // options columns = ['*'], @@ -253,7 +188,7 @@ export class TagService extends BaseService { ) => { const tag = await this.baseFindOrCreate({ where: { content }, - data: { content, cover, creator, description, editors, owner }, + data: { content, creator }, table: this.table, columns, modifier: (builder: Knex.QueryBuilder) => { @@ -270,98 +205,6 @@ export class TagService extends BaseService { return tag } - /** - * Count of a tag's participants. - * - */ - public countParticipants = async ({ - id, - exclude, - }: { - id: string - exclude?: string[] - }) => { - 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 = ? - ) AS base`, - [id] - ) - - const result = await this.knex - .from(function (this: Knex.QueryBuilder) { - this.select('author_id') - .from(subquery) - .groupBy('author_id') - .as('source') - - if (exclude) { - this.whereNotIn('author_id', exclude) - } - }) - .count() - .first() - - return parseInt(result ? (result.count as string) : '0', 10) - } - - /** - * Find a tag's participants. - * - */ - public findParticipants = async ({ - id, - skip, - take, - exclude, - }: { - id: string - skip?: number - take?: number - exclude?: string[] - }) => { - 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 = ? - ORDER BY - at.created_at - ) AS base`, - [id] - ) - - const query = this.knex - .select('author_id') - .from(subquery) - .groupBy('author_id') - - if (exclude) { - query.whereNotIn('author_id', exclude) - } - - if (skip !== undefined && Number.isFinite(skip)) { - query.offset(skip) - } - if (take !== undefined && Number.isFinite(take)) { - query.limit(take) - } - - return query - } - /********************************* * * * Follow * @@ -404,35 +247,6 @@ export class TagService extends BaseService { }) .del() - /** - * Find followers of a tag using id as pagination index. - * - */ - public findFollowers = async ({ - targetId, - skip, - take, - }: { - targetId: string - skip?: string - take?: number - }) => { - 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 - } - public isActionEnabled = async ({ userId, action, @@ -475,14 +289,6 @@ export class TagService extends BaseService { : this.knex.from('action_tag').where(data).del() } - public countFollowers = async (targetId: string) => { - 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 * @@ -694,59 +500,30 @@ export class TagService extends BaseService { return tag.tagScore || 0 } + public countTopTags = async () => { + const result = await this.knex(MATERIALIZED_VIEW.tag_stats_materialized) + .count() + .first() + return parseInt(result ? (result.count as string) : '0', 10) + } + public findTopTags = ({ take = 50, skip, - top = 'r3m', minAuthors, }: { take?: number skip?: number - // recent 1 week, 1 month, or 3 months? - top?: 'r1w' | 'r2w' | 'r1m' | 'r3m' minAuthors?: number }): Promise> => this.knex - .select('id') - .from(MATERIALIZED_VIEW.tags_lasts_view_materialized) - .modify(function (this: Knex.QueryBuilder) { + .select('tag_id as id') + .from(MATERIALIZED_VIEW.tag_stats_materialized) + .modify((builder: Knex.QueryBuilder) => { if (minAuthors) { - this.where('num_authors', '>=', minAuthors) - } - switch (top) { - case 'r1w': - this.orderByRaw( - 'num_authors_r1w DESC NULLS LAST, num_articles_r1w DESC NULLS LAST' - ) - // no break to fallthrough - case 'r2w': - this.orderByRaw( - 'num_authors_r2w DESC NULLS LAST, num_articles_r2w DESC NULLS LAST' - ) - // no break to fallthrough - case 'r1m': - this.orderByRaw( - 'num_authors_r1m DESC NULLS LAST, num_articles_r1m DESC NULLS LAST' - ) - // no break to fallthrough - case 'r3m': - // always use recent3months as fallback - this.orderByRaw( - 'num_authors_r3m DESC NULLS LAST, num_articles_r3m DESC NULLS LAST' - ) - /* this orderBy does not work as documented - .orderBy([ - { column: 'num_authors_r3m', order: 'desc', nulls: 'last' }, - { column: 'num_articles_r3m', order: 'desc', nulls: 'last' }, - { column: 'span_days', order: 'desc', nulls: 'last' }, - ]) - */ + builder.where('all_users', '>=', minAuthors) } }) - // last fallback - .orderByRaw('num_authors DESC NULLS LAST, num_articles DESC NULLS LAST') - .orderByRaw('span_days DESC NULLS LAST') - .orderByRaw('created_at') // ascending from earliest to latest .modify((builder: Knex.QueryBuilder) => { if (skip !== undefined && Number.isFinite(skip)) { builder.offset(skip) @@ -787,16 +564,6 @@ export class TagService extends BaseService { return parseInt(result ? (result.count as string) : '0', 10) } - public addTagRecommendation = (tagId: string) => - this.baseFindOrCreate({ - where: { tagId }, - data: { tagId }, - table: 'matters_choice_tag', - }) - - public removeTagRecommendation = (tagId: string) => - this.knex('matters_choice_tag').where({ tagId }).del() - /********************************* * * * Article * @@ -804,28 +571,22 @@ export class TagService extends BaseService { *********************************/ public createArticleTags = async ({ articleIds, - creator, tagIds, - selected, + creator, }: { articleIds: string[] - creator: string tagIds: string[] - selected?: boolean + creator: string }) => { articleIds = Array.from(new Set(articleIds)) tagIds = Array.from(new Set(tagIds)) const items = articleIds .map((articleId) => - tagIds.map((tagId) => ({ - articleId, - creator, - tagId, - ...(selected === true ? { selected } : {}), - })) + tagIds.map((tagId) => ({ articleId, creator, tagId })) ) .flat(1) + return this.baseBatchCreate(items, 'article_tag') } @@ -849,44 +610,11 @@ export class TagService extends BaseService { /** * Count article authors by a given tag id. */ - public countAuthors = async ({ - id: tagId, - selected, - withSynonyms = true, - }: { - id: string - selected?: boolean - withSynonyms?: boolean - }) => { - const knex = this.knex - - let result: any - try { - result = await this.knex(MATERIALIZED_VIEW.tags_lasts_view_materialized) - .select('id', 'content', 'id_slug', 'num_authors', 'num_articles') - .where(function (this: Knex.QueryBuilder) { - this.where('id', '=', tagId) - if (withSynonyms) { - this.orWhere(knex.raw(`dup_tag_ids @> ARRAY[?] ::int[]`, [tagId])) - } // else { this.where('id', tagId) // exactly } - }) - .first() - } catch (err) { - // empty; do nothing - } - - if (result?.numAuthors) { - return parseInt(result.numAuthors ?? '0', 10) - } - - result = await this.knex('article_tag') + public countAuthors = async ({ id: tagId }: { id: string }) => { + const result = await this.knex('article_tag') .join('article', 'article_id', 'article.id') .countDistinct('author_id') - .where({ - // 'article_tag.tag_id': tagId, - tagId, - state: ARTICLE_STATE.active, - }) + .where({ tagId, state: ARTICLE_STATE.active }) .first() return parseInt(result ? (result.count as string) : '0', 10) @@ -895,47 +623,85 @@ export class TagService extends BaseService { /** * Count articles by a given tag id. */ - public countArticles = async ({ + public countArticles = async ({ id: tagId }: { id: string }) => { + const result = await this.knexRO('article_tag') + .join('article', 'article_id', 'article.id') + .countDistinct('article_id') + .first() + .where({ tagId, state: ARTICLE_STATE.active }) + + return parseInt(result ? (result.count as string) : '0', 10) + } + + private getHottestArticlesBaseQuery = (tagId: string) => { + return this.knexRO.with('tagged_articles', (builder) => + builder + .select('article.id', 'article_stats.reads', 'article_stats.claps') + .from('article_tag') + .innerJoin('article', 'article.id', 'article_tag.article_id') + .innerJoin( + 'article_version_newest as avn', + 'avn.article_id', + 'article_tag.article_id' + ) + .leftJoin( + 'article_stats_materialized as article_stats', + 'article_stats.article_id', + 'article_tag.article_id' + ) + .where({ + 'article_tag.tag_id': tagId, + 'article.state': ARTICLE_STATE.active, + }) + ) + } + + public findHottestArticleIds = async ({ id: tagId, - selected, - withSynonyms = true, + skip, + take, }: { id: string - selected?: boolean - withSynonyms?: boolean + skip?: number + take?: number }) => { - const knexRO = this.knexRO + const hasHottest = await this.knex( + MATERIALIZED_VIEW.tag_hottest_materialized + ) + .where({ tagId }) + .first() - let result: any - try { - result = await this.knexRO(MATERIALIZED_VIEW.tags_lasts_view_materialized) - .select('id', 'content', 'id_slug', 'num_authors', 'num_articles') - .where(function (this: Knex.QueryBuilder) { - this.where('tag_id', tagId) - if (withSynonyms) { - this.orWhere(knexRO.raw(`dup_tag_ids @> ARRAY[?] ::int[]`, [tagId])) - } // else { this.where('id', tagId) // exactly } - }) - .first() - } catch (err) { - // empty; do nothing + if (!hasHottest) { + return [] } - if (result?.numArticles) { - return parseInt(result.numArticles ?? '0', 10) - } + const results = await this.getHottestArticlesBaseQuery(tagId) + .select('id as article_id') + .orderByRaw('(COALESCE(reads, 0) + COALESCE(claps, 0)) DESC') + .modify((builder: Knex.QueryBuilder) => { + if (skip !== undefined && Number.isFinite(skip)) { + builder.offset(skip) + } + if (take !== undefined && Number.isFinite(take)) { + builder.limit(take) + } + }) - result = await this.knexRO('article_tag') - .join('article', 'article_id', 'article.id') - .countDistinct('article_id') + return results.map(({ articleId }: { articleId: string }) => articleId) + } + + public countHottestArticles = async ({ id: tagId }: { id: string }) => { + const hasHottest = await this.knex( + MATERIALIZED_VIEW.tag_hottest_materialized + ) + .where({ tagId }) .first() - .where({ - // tagId: id, - // 'article_tag.tag_id': tagId, - tagId, - state: ARTICLE_STATE.active, - ...(selected === true ? { selected } : {}), - }) + + if (!hasHottest) { + return 0 + } + + const result = await this.getHottestArticlesBaseQuery(tagId).count().first() return parseInt(result ? (result.count as string) : '0', 10) } @@ -945,18 +711,12 @@ export class TagService extends BaseService { */ public findArticleIds = async ({ id: tagId, - selected, - sortBy, - withSynonyms, excludeRestricted, excludeSpam, skip, take, }: { id: string - selected?: boolean - sortBy?: 'byHottestDesc' | 'byCreatedAtDesc' - withSynonyms?: boolean excludeRestricted?: boolean excludeSpam?: boolean skip?: number @@ -970,19 +730,9 @@ export class TagService extends BaseService { .join('article', 'article_id', 'article.id') .where({ state: ARTICLE_STATE.active, - ...(selected === true ? { selected } : {}), }) .andWhere((builder: Knex.QueryBuilder) => { builder.where('tag_id', tagId) - if (withSynonyms) { - builder.orWhereIn( - 'tag_id', - this.knexRO - .from(MATERIALIZED_VIEW.tags_lasts_view_materialized) - .whereRaw('dup_tag_ids @> ARRAY[?] ::int[]', tagId) - .select(this.knex.raw('UNNEST(dup_tag_ids)')) - ) - } }) .modify((builder: Knex.QueryBuilder) => { if (excludeRestricted) { @@ -1003,16 +753,7 @@ export class TagService extends BaseService { if (excludeSpam) { builder.modify(excludeSpamModifier, spamThreshold) } - if (sortBy === 'byHottestDesc') { - builder - .join( - // instead of leftJoin, only shows articles from materialized - 'article_hottest_materialized AS ah', - 'ah.id', - 'article.id' - ) - .orderByRaw(`score DESC NULLS LAST`) - } + builder.orderBy('article.id', 'desc') if (skip !== undefined && Number.isFinite(skip)) { @@ -1038,18 +779,6 @@ export class TagService extends BaseService { return result.map(({ articleId }: { articleId: string }) => articleId) } - public deleteArticleTagsByArticleIds = async ({ - articleIds, - tagId, - }: { - articleIds: string[] - tagId: string - }) => - this.knex('article_tag') - .whereIn('article_id', articleIds) - .andWhere({ tagId }) - .del() - public deleteArticleTagsByTagIds = async ({ articleId, tagIds, @@ -1062,53 +791,6 @@ export class TagService extends BaseService { .andWhere({ articleId }) .del() - public isArticleSelected = async ({ - articleId, - tagId, - }: { - articleId: string - tagId: string - }) => { - const result = await this.knex('article_tag').where({ - articleId, - tagId, - selected: true, - }) - return result.length > 0 - } - - /** - * Find article covers by tag id. - */ - public findArticleCovers = async ({ - id, - }: { - id: string - }): Promise> => { - const cache = new CacheService(CACHE_PREFIX.TAG_COVERS, this.redis) - return cache.getObject({ - keys: { id }, - expire: CACHE_TTL.MEDIUM, - getter: async () => - this.knexRO - .select('article_version_newest.cover') - .from('article_tag') - .join( - 'article_version_newest', - 'article_tag.article_id', - 'article_version_newest.article_id' - ) - .join('article', 'article_tag.article_id', 'article.id') - .whereNotNull('article_version_newest.cover') - .andWhere({ - tagId: id, - state: ARTICLE_STATE.active, - }) - .limit(DEFAULT_TAKE_PER_PAGE) - .orderBy('article_tag.id', 'asc'), - }) - } - /********************************* * * * OSS * @@ -1126,21 +808,21 @@ export class TagService extends BaseService { tagIds, content, creator, - editors, - owner, }: { tagIds: string[] content: string creator: string - editors: string[] - owner: string }) => { // create new tag - const newTag = await this.create({ content, creator, editors, owner }) + const newTag = await this.create({ content, creator }) // move article tags to new tag const articleIds = await this.findArticleIdsByTagIds(tagIds) - await this.createArticleTags({ articleIds, creator, tagIds: [newTag.id] }) + await this.createArticleTags({ + articleIds, + tagIds: [newTag.id], + creator, + }) // delete article tags await this.knex('article_tag').whereIn('tag_id', tagIds).del() @@ -1225,19 +907,11 @@ export class TagService extends BaseService { } // create tag records - const tagEditors = environment.mattyId - ? [environment.mattyId, article.authorId] - : [article.authorId] const dbTags = ( await Promise.all( tags.filter(Boolean).map(async (content: string) => this.create( - { - content, - creator: article.authorId, - editors: tagEditors, - owner: article.authorId, - }, + { content, creator: article.authorId }, { columns: ['id', 'content'], skipCreate: normalizeTagInput(content) !== content, // || content.length > MAX_TAG_CONTENT_LENGTH, @@ -1260,8 +934,8 @@ export class TagService extends BaseService { // add await this.createArticleTags({ articleIds: [article.id], - creator: article.authorId, tagIds: addIds, + creator: article.authorId, }) // delete unwanted diff --git a/src/connectors/userService.ts b/src/connectors/userService.ts index 686cb5030..848183368 100644 --- a/src/connectors/userService.ts +++ b/src/connectors/userService.ts @@ -685,15 +685,15 @@ export class UserService extends BaseService { await trx('push_device').where({ userId: id }).del() // remove tag owner and editors - await trx.raw(` - UPDATE - tag - SET - owner = NULL, - editors = array_remove(editors, owner::text) - WHERE - owner = ${id} - `) + // await trx.raw(` + // UPDATE + // tag + // SET + // owner = NULL, + // editors = array_remove(editors, owner::text) + // WHERE + // owner = ${id} + // `) return user }) @@ -1894,76 +1894,6 @@ export class UserService extends BaseService { return user } - public updateLiker = ({ - likerId, - ...data - }: { - [key: string]: any - likerId: string - }) => - this.knex - .select() - .from('user_oauth_likecoin') - .where({ likerId }) - .update(data) - - // register a new LikerId by a given userName - public registerLikerId = async ({ - userId, - userName, - ip, - }: { - userId: string - userName: string - ip?: string - }) => { - // check - const likerId = await this.likecoin.check({ user: userName }) - - // register - const oAuthService = new OAuthService(this.connections) - const tokens = await oAuthService.generateTokenForLikeCoin({ userId }) - const { accessToken, refreshToken, scope } = await this.likecoin.register({ - user: likerId, - token: tokens.accessToken, - ip, - }) - - // save to db - return this.saveLiker({ - userId, - likerId, - accountType: 'general', - accessToken, - refreshToken, - scope, - }) - } - - // Promote a platform temp LikerID - public claimLikerId = async ({ - userId, - liker, - ip, - }: { - userId: string - liker: UserOAuthLikeCoin - ip?: string - }) => { - const oAuthService = new OAuthService(this.connections) - const tokens = await oAuthService.generateTokenForLikeCoin({ userId }) - - await this.likecoin.edit({ - action: 'claim', - payload: { user: liker.likerId, platformToken: tokens.accessToken }, - ip, - }) - - return this.knex('user_oauth_likecoin') - .where({ likerId: liker.likerId }) - .update({ accountType: 'general' }) - } - // Transfer a platform temp LikerID's LIKE and binding to target LikerID public transferLikerId = async ({ fromLiker, diff --git a/src/definitions/index.d.ts b/src/definitions/index.d.ts index 9abf58bea..9fd4f6254 100644 --- a/src/definitions/index.d.ts +++ b/src/definitions/index.d.ts @@ -280,7 +280,8 @@ type OtherTable = | 'user_notify_setting' export type View = - | 'tag_count_view' + // | 'tag_count_view' + | 'tag_stats_view' | 'user_reader_view' | 'article_count_view' | 'article_hottest_view' @@ -290,7 +291,8 @@ export type View = export type MaterializedView = | 'article_count_materialized' - | 'tag_count_materialized' + // | 'tag_count_materialized' + | 'tag_stats_materialized' | 'user_reader_materialized' | 'article_value_materialized' | 'featured_comment_materialized' diff --git a/src/definitions/schema.d.ts b/src/definitions/schema.d.ts index 528f721cf..c8f86e36a 100644 --- a/src/definitions/schema.d.ts +++ b/src/definitions/schema.d.ts @@ -73,12 +73,6 @@ export type Scalars = { Upload: { input: any; output: any } } -export type GQLAddArticlesTagsInput = { - articles?: InputMaybe> - id: Scalars['ID']['input'] - selected?: InputMaybe -} - export type GQLAddCollectionsArticlesInput = { articles: Array collections: Array @@ -190,6 +184,7 @@ export type GQLArticle = GQLNode & author: GQLUser /** Available translation languages. */ availableTranslations?: Maybe> + bookmarked: Scalars['Boolean']['output'] /** associated campaigns */ campaigns: Array /** whether readers can comment */ @@ -220,11 +215,6 @@ export type GQLArticle = GQLNode & donationCount: Scalars['Int']['output'] /** Donations of this article, grouped by sender */ donations: GQLArticleDonationConnection - /** - * Drafts linked to this article. - * @deprecated Use Article.newestUnpublishedDraft or Article.newestPublishedDraft instead - */ - drafts?: Maybe> /** List of featured comments of this article. */ featuredComments: GQLCommentConnection /** This value determines if current viewer has appreciated or not. */ @@ -241,15 +231,12 @@ export type GQLArticle = GQLNode & license: GQLArticleLicenseType /** Media hash, composed of cid encoding, of this article. */ mediaHash: Scalars['String']['output'] - /** Newest published draft linked to this article. */ - newestPublishedDraft: GQLDraft - /** Newest unpublished draft linked to this article. */ - newestUnpublishedDraft?: Maybe oss: GQLArticleOss /** The number determines how many comments can be set as pinned comment. */ pinCommentLeft: Scalars['Int']['output'] /** The number determines how many pinned comments can be set. */ pinCommentLimit: Scalars['Int']['output'] + /** This value determines if this article is an author selected article or not. */ pinned: Scalars['Boolean']['output'] /** List of pinned comments. */ pinnedComments?: Maybe> @@ -286,14 +273,10 @@ export type GQLArticle = GQLNode & /** State of this article. */ state: GQLArticleState /** - * This value determines if this article is an author selected article or not. - * @deprecated Use pinned instead + * This value determines if current Viewer has bookmarked of not. + * @deprecated Use bookmarked instead */ - sticky: Scalars['Boolean']['output'] - /** This value determines if current Viewer has subscribed of not. */ subscribed: Scalars['Boolean']['output'] - /** Subscribers of this article. */ - subscribers: GQLUserConnection /** A short summary for this article. */ summary: Scalars['String']['output'] /** This value determines if the summary is customized or not. */ @@ -394,14 +377,6 @@ export type GQLArticleResponsesArgs = { input: GQLResponsesInput } -/** - * This type contains metadata, content, hash and related data of an article. If you - * want information about article's comments. Please check Comment type. - */ -export type GQLArticleSubscribersArgs = { - input: GQLConnectionArgs -} - /** * This type contains metadata, content, hash and related data of an article. If you * want information about article's comments. Please check Comment type. @@ -830,13 +805,6 @@ export type GQLCampaignsInput = { export type GQLChain = 'Optimism' | 'Polygon' -export type GQLChangeEmailInput = { - newEmail: Scalars['String']['input'] - newEmailCodeId: Scalars['ID']['input'] - oldEmail: Scalars['String']['input'] - oldEmailCodeId: Scalars['ID']['input'] -} - export type GQLCircle = GQLNode & { __typename?: 'Circle' /** Analytics dashboard. */ @@ -1339,11 +1307,6 @@ export type GQLDeleteAnnouncementsInput = { ids?: InputMaybe> } -export type GQLDeleteArticlesTagsInput = { - articles?: InputMaybe> - id: Scalars['ID']['input'] -} - export type GQLDeleteCollectionArticlesInput = { articles: Array collection: Scalars['ID']['input'] @@ -1479,8 +1442,6 @@ export type GQLEditArticleInput = { requestForDonation?: InputMaybe sensitive?: InputMaybe state?: InputMaybe - /** deprecated, use pinned instead */ - sticky?: InputMaybe summary?: InputMaybe tags?: InputMaybe> title?: InputMaybe @@ -1734,11 +1695,6 @@ export type GQLLiker = { civicLiker: Scalars['Boolean']['output'] /** Liker ID of LikeCoin */ likerId?: Maybe - /** - * Rate of LikeCoin/USD - * @deprecated No longer in use - */ - rateUSD?: Maybe /** Total LIKE left in wallet. */ total: Scalars['Float']['output'] } @@ -1836,8 +1792,6 @@ export type GQLMonthlyDatum = { export type GQLMutation = { __typename?: 'Mutation' - /** Add one tag to articles. */ - addArticlesTags: GQLTag /** Add blocked search keyword to blocked_search_word db */ addBlockedSearchKeyword: GQLBlockedSearchKeyword /** Add articles to the begining of the collections. */ @@ -1851,11 +1805,6 @@ export type GQLMutation = { applyCampaign: GQLCampaign /** Appreciate an article. */ appreciateArticle: GQLArticle - /** - * Change user email. - * @deprecated use 'setEmail' instead - */ - changeEmail: GQLUser /** Let Traveloggers owner claims a Logbook, returns transaction hash */ claimLogbooks: GQLClaimLogbooksResult /** Clear read history for user. */ @@ -1867,8 +1816,6 @@ export type GQLMutation = { /** Create Stripe Connect account for Payout */ connectStripeAccount: GQLConnectStripeAccountResult deleteAnnouncements: Scalars['Boolean']['output'] - /** Delete one tag from articles */ - deleteArticlesTags: GQLTag /** Delete blocked search keywords from search_history db */ deleteBlockedSearchKeywords?: Maybe /** Remove articles from the collection. */ @@ -1883,12 +1830,8 @@ export type GQLMutation = { directImageUpload: GQLAsset /** Edit an article. */ editArticle: GQLArticle + /** Login user. */ emailLogin: GQLAuthResult - /** - * Generate or claim a Liker ID through LikeCoin - * @deprecated No longer in use - */ - generateLikerId: GQLUser /** Get signing message. */ generateSigningMessage: GQLSigningMessageResult /** Invite others to join circle */ @@ -1932,8 +1875,6 @@ export type GQLMutation = { putRemark?: Maybe putRestrictedUsers: Array putSkippedListItem?: Maybe> - /** Create or update tag. */ - putTag: GQLTag putWritingChallenge: GQLWritingChallenge /** Read an article. */ readArticle: GQLArticle @@ -1950,11 +1891,6 @@ export type GQLMutation = { resetLikerId: GQLUser /** Reset user or payment password. */ resetPassword?: Maybe - /** - * Reset crypto wallet. - * @deprecated use 'removeWalletLogin' instead - */ - resetWallet: GQLUser sendCampaignAnnouncement?: Maybe /** Send verification code for email. */ sendVerificationCode?: Maybe @@ -1980,21 +1916,28 @@ export type GQLMutation = { toggleArticleRecommend: GQLArticle /** Block or Unblock a given user. */ toggleBlockUser: GQLUser + toggleBookmarkArticle: GQLArticle + toggleBookmarkTag: GQLTag /** * Follow or unfollow a Circle. * @deprecated No longer in use */ toggleFollowCircle: GQLCircle - /** Follow or unfollow tag. */ + /** + * Bookmark or unbookmark tag. + * @deprecated Use toggleBookmarkTag instead + */ toggleFollowTag: GQLTag /** Follow or Unfollow current user. */ toggleFollowUser: GQLUser /** Pin or Unpin a comment. */ togglePinComment: GQLComment toggleSeedingUsers: Array> - /** Subscribe or Unsubscribe article */ + /** + * Bookmark or unbookmark article + * @deprecated Use toggleBookmarkArticle instead + */ toggleSubscribeArticle: GQLArticle - toggleTagRecommend: GQLTag toggleUsersBadge: Array> toggleWritingChallengeFeaturedArticles: GQLCampaign unbindLikerId: GQLUser @@ -2008,15 +1951,11 @@ export type GQLMutation = { unvoteComment: GQLComment updateArticleSensitive: GQLArticle updateArticleState: GQLArticle - /** Update articles' tag. */ - updateArticlesTags: GQLTag updateCampaignApplicationState: GQLCampaign /** Update a comments' state. */ updateCommentsState: Array /** Update user notification settings. */ updateNotificationSetting: GQLUser - /** Update member, permission and othters of a tag. */ - updateTagSetting: GQLTag /** Update referralCode of a user, used in OSS. */ updateUserExtra: GQLUser /** Update user information. */ @@ -2025,18 +1964,8 @@ export type GQLMutation = { updateUserRole: GQLUser /** Update state of a user, used in OSS. */ updateUserState?: Maybe> - /** - * Login user. - * @deprecated use 'emailLogin' instead - */ - userLogin: GQLAuthResult /** Logout user. */ userLogout: Scalars['Boolean']['output'] - /** - * Register user, can only be used on matters.{town,news} website. - * @deprecated use 'emailLogin' instead - */ - userRegister: GQLAuthResult /** Verify user email. */ verifyEmail: GQLAuthResult /** Upvote or downvote a comment. */ @@ -2045,10 +1974,6 @@ export type GQLMutation = { walletLogin: GQLAuthResult } -export type GQLMutationAddArticlesTagsArgs = { - input: GQLAddArticlesTagsInput -} - export type GQLMutationAddBlockedSearchKeywordArgs = { input: GQLKeywordInput } @@ -2077,10 +2002,6 @@ export type GQLMutationAppreciateArticleArgs = { input: GQLAppreciateArticleInput } -export type GQLMutationChangeEmailArgs = { - input: GQLChangeEmailInput -} - export type GQLMutationClaimLogbooksArgs = { input: GQLClaimLogbooksInput } @@ -2101,10 +2022,6 @@ export type GQLMutationDeleteAnnouncementsArgs = { input: GQLDeleteAnnouncementsInput } -export type GQLMutationDeleteArticlesTagsArgs = { - input: GQLDeleteArticlesTagsInput -} - export type GQLMutationDeleteBlockedSearchKeywordsArgs = { input: GQLKeywordsInput } @@ -2241,10 +2158,6 @@ export type GQLMutationPutSkippedListItemArgs = { input: GQLPutSkippedListItemInput } -export type GQLMutationPutTagArgs = { - input: GQLPutTagInput -} - export type GQLMutationPutWritingChallengeArgs = { input: GQLPutWritingChallengeInput } @@ -2277,10 +2190,6 @@ export type GQLMutationResetPasswordArgs = { input: GQLResetPasswordInput } -export type GQLMutationResetWalletArgs = { - input: GQLResetWalletInput -} - export type GQLMutationSendCampaignAnnouncementArgs = { input: GQLSendCampaignAnnouncementInput } @@ -2341,6 +2250,14 @@ export type GQLMutationToggleBlockUserArgs = { input: GQLToggleItemInput } +export type GQLMutationToggleBookmarkArticleArgs = { + input: GQLToggleItemInput +} + +export type GQLMutationToggleBookmarkTagArgs = { + input: GQLToggleItemInput +} + export type GQLMutationToggleFollowCircleArgs = { input: GQLToggleItemInput } @@ -2365,10 +2282,6 @@ export type GQLMutationToggleSubscribeArticleArgs = { input: GQLToggleItemInput } -export type GQLMutationToggleTagRecommendArgs = { - input: GQLToggleRecommendInput -} - export type GQLMutationToggleUsersBadgeArgs = { input: GQLToggleUsersBadgeInput } @@ -2409,10 +2322,6 @@ export type GQLMutationUpdateArticleStateArgs = { input: GQLUpdateArticleStateInput } -export type GQLMutationUpdateArticlesTagsArgs = { - input: GQLUpdateArticlesTagsInput -} - export type GQLMutationUpdateCampaignApplicationStateArgs = { input: GQLUpdateCampaignApplicationStateInput } @@ -2425,10 +2334,6 @@ export type GQLMutationUpdateNotificationSettingArgs = { input: GQLUpdateNotificationSettingInput } -export type GQLMutationUpdateTagSettingArgs = { - input: GQLUpdateTagSettingInput -} - export type GQLMutationUpdateUserExtraArgs = { input: GQLUpdateUserExtraInput } @@ -2445,14 +2350,6 @@ export type GQLMutationUpdateUserStateArgs = { input: GQLUpdateUserStateInput } -export type GQLMutationUserLoginArgs = { - input: GQLUserLoginInput -} - -export type GQLMutationUserRegisterArgs = { - input: GQLUserRegisterInput -} - export type GQLMutationVerifyEmailArgs = { input: GQLVerifyEmailInput } @@ -2921,13 +2818,6 @@ export type GQLPutSkippedListItemInput = { value?: InputMaybe } -export type GQLPutTagInput = { - content?: InputMaybe - cover?: InputMaybe - description?: InputMaybe - id?: InputMaybe -} - export type GQLPutWritingChallengeInput = { announcements?: InputMaybe> applicationPeriod?: InputMaybe @@ -3079,11 +2969,6 @@ export type GQLRecommendation = { /** Global circles sort by created time. */ newestCircles: GQLCircleConnection newestExcludeSpam: GQLArticleConnection - /** - * Articles recommended based on recently read article tags. - * @deprecated Merged into following - */ - readTagsArticles: GQLArticleConnection /** Selected tag list */ selectedTags: GQLTagConnection /** Global tag list, sort by activities in recent 14 days. */ @@ -3130,10 +3015,6 @@ export type GQLRecommendationNewestExcludeSpamArgs = { input: GQLConnectionArgs } -export type GQLRecommendationReadTagsArticlesArgs = { - input: GQLConnectionArgs -} - export type GQLRecommendationSelectedTagsArgs = { input: GQLRecommendInput } @@ -3237,10 +3118,6 @@ export type GQLResetPasswordInput = { export type GQLResetPasswordType = 'account' | 'payment' -export type GQLResetWalletInput = { - id: Scalars['ID']['input'] -} - export type GQLResponse = GQLArticle | GQLComment export type GQLResponseConnection = GQLConnection & { @@ -3526,38 +3403,22 @@ export type GQLTag = GQLNode & { articlesExcludeSpam: GQLArticleConnection /** Content of this tag. */ content: Scalars['String']['output'] - /** Tag's cover link. */ - cover?: Maybe /** Time of this tag was created. */ createdAt: Scalars['DateTime']['output'] - /** Creator of this tag. */ - creator?: Maybe deleted: Scalars['Boolean']['output'] - /** Description of this tag. */ - description?: Maybe - /** Editors of this tag. */ - editors?: Maybe> - /** Followers of this tag. */ - followers: GQLUserConnection /** Unique id of this tag. */ id: Scalars['ID']['output'] /** This value determines if current viewer is following or not. */ isFollower?: Maybe - /** This value determines if it is official. */ - isOfficial?: Maybe /** Counts of this tag. */ numArticles: Scalars['Int']['output'] numAuthors: Scalars['Int']['output'] oss: GQLTagOss - /** Owner of this tag. */ - owner?: Maybe - /** Participants of this tag. */ - participants: GQLUserConnection /** Tags recommended based on relations to current tag. */ recommended: GQLTagConnection + /** Authors recommended based on relations to current tag. */ + recommendedAuthors: GQLUserConnection remark?: Maybe - /** This value determines if this article is selected by this tag or not. */ - selected: Scalars['Boolean']['output'] } /** This type contains content, count and related data of an article tag. */ @@ -3570,36 +3431,20 @@ export type GQLTagArticlesExcludeSpamArgs = { input: GQLTagArticlesInput } -/** This type contains content, count and related data of an article tag. */ -export type GQLTagEditorsArgs = { - input?: InputMaybe -} - -/** This type contains content, count and related data of an article tag. */ -export type GQLTagFollowersArgs = { - input: GQLConnectionArgs -} - -/** This type contains content, count and related data of an article tag. */ -export type GQLTagParticipantsArgs = { - input: GQLConnectionArgs -} - /** This type contains content, count and related data of an article tag. */ export type GQLTagRecommendedArgs = { input: GQLConnectionArgs } /** This type contains content, count and related data of an article tag. */ -export type GQLTagSelectedArgs = { - input: GQLTagSelectedInput +export type GQLTagRecommendedAuthorsArgs = { + input: GQLConnectionArgs } export type GQLTagArticlesInput = { after?: InputMaybe first?: InputMaybe oss?: InputMaybe - selected?: InputMaybe sortBy?: InputMaybe } @@ -3618,21 +3463,10 @@ export type GQLTagEdge = { node: GQLTag } -export type GQLTagEditorsInput = { - excludeAdmin?: InputMaybe - excludeOwner?: InputMaybe -} - export type GQLTagOss = { __typename?: 'TagOSS' boost: Scalars['Float']['output'] score: Scalars['Float']['output'] - selected: Scalars['Boolean']['output'] -} - -export type GQLTagSelectedInput = { - id?: InputMaybe - mediaHash?: InputMaybe } export type GQLTagsInput = { @@ -3863,12 +3697,6 @@ export type GQLUpdateArticleStateInput = { state: GQLArticleState } -export type GQLUpdateArticlesTagsInput = { - articles?: InputMaybe> - id: Scalars['ID']['input'] - isSelected: Scalars['Boolean']['input'] -} - export type GQLUpdateCampaignApplicationStateInput = { campaign: Scalars['ID']['input'] state: GQLCampaignApplicationState @@ -3885,19 +3713,6 @@ export type GQLUpdateNotificationSettingInput = { type: GQLNotificationSettingType } -export type GQLUpdateTagSettingInput = { - editors?: InputMaybe> - id: Scalars['ID']['input'] - type: GQLUpdateTagSettingType -} - -export type GQLUpdateTagSettingType = - | 'add_editor' - | 'adopt' - | 'leave' - | 'leave_editor' - | 'remove_editor' - export type GQLUpdateUserExtraInput = { id: Scalars['ID']['input'] referralCode?: InputMaybe @@ -3913,8 +3728,6 @@ export type GQLUpdateUserInfoInput = { paymentPointer?: InputMaybe profileCover?: InputMaybe referralCode?: InputMaybe - /** @deprecated use 'setUserName' instead */ - userName?: InputMaybe } export type GQLUpdateUserRoleInput = { @@ -4203,11 +4016,6 @@ export type GQLUserInput = { export type GQLUserLanguage = 'en' | 'zh_hans' | 'zh_hant' -export type GQLUserLoginInput = { - email: Scalars['String']['input'] - password: Scalars['String']['input'] -} - export type GQLUserNotice = GQLNotice & { __typename?: 'UserNotice' /** List of notice actors. */ @@ -4259,16 +4067,6 @@ export type GQLUserRecommendationActivity = { export type GQLUserRecommendationActivitySource = 'UserFollowing' -export type GQLUserRegisterInput = { - codeId: Scalars['ID']['input'] - description?: InputMaybe - displayName: Scalars['String']['input'] - email: Scalars['String']['input'] - password: Scalars['String']['input'] - referralCode?: InputMaybe - userName?: InputMaybe -} - export type GQLUserRestriction = { __typename?: 'UserRestriction' createdAt: Scalars['DateTime']['output'] @@ -4325,10 +4123,7 @@ export type GQLUserStatus = { export type GQLVerificationCodeType = | 'email_otp' - | 'email_reset' - | 'email_reset_confirm' | 'email_verify' - | 'password_reset' | 'payment_password_reset' | 'register' @@ -4362,16 +4157,6 @@ export type GQLWalletTransactionsArgs = { } export type GQLWalletLoginInput = { - /** - * email verification code, required for wallet register - * @deprecated No longer in use - */ - codeId?: InputMaybe - /** - * required for wallet register - * @deprecated No longer in use - */ - email?: InputMaybe ethAddress: Scalars['String']['input'] /** used in register */ language?: InputMaybe @@ -4697,7 +4482,6 @@ export type GQLResolversInterfaceTypes< /** Mapping between all available schema types and the resolvers types */ export type GQLResolversTypes = ResolversObject<{ - AddArticlesTagsInput: GQLAddArticlesTagsInput AddCollectionsArticlesInput: GQLAddCollectionsArticlesInput AddCreditInput: GQLAddCreditInput AddCreditResult: ResolverTypeWrapper< @@ -4842,7 +4626,6 @@ export type GQLResolversTypes = ResolversObject<{ CampaignState: GQLCampaignState CampaignsInput: GQLCampaignsInput Chain: GQLChain - ChangeEmailInput: GQLChangeEmailInput Circle: ResolverTypeWrapper CircleAnalytics: ResolverTypeWrapper CircleConnection: ResolverTypeWrapper< @@ -4919,7 +4702,6 @@ export type GQLResolversTypes = ResolversObject<{ DatetimeRange: ResolverTypeWrapper DatetimeRangeInput: GQLDatetimeRangeInput DeleteAnnouncementsInput: GQLDeleteAnnouncementsInput - DeleteArticlesTagsInput: GQLDeleteArticlesTagsInput DeleteCollectionArticlesInput: GQLDeleteCollectionArticlesInput DeleteCollectionsInput: GQLDeleteCollectionsInput DeleteCommentInput: GQLDeleteCommentInput @@ -5106,7 +4888,6 @@ export type GQLResolversTypes = ResolversObject<{ PutRemarkInput: GQLPutRemarkInput PutRestrictedUsersInput: GQLPutRestrictedUsersInput PutSkippedListItemInput: GQLPutSkippedListItemInput - PutTagInput: GQLPutTagInput PutWritingChallengeInput: GQLPutWritingChallengeInput Query: ResolverTypeWrapper<{}> QuoteCurrency: GQLQuoteCurrency @@ -5152,7 +4933,6 @@ export type GQLResolversTypes = ResolversObject<{ ResetLikerIdInput: GQLResetLikerIdInput ResetPasswordInput: GQLResetPasswordInput ResetPasswordType: GQLResetPasswordType - ResetWalletInput: GQLResetWalletInput Response: ResolverTypeWrapper< GQLResolversUnionTypes['Response'] > @@ -5212,9 +4992,7 @@ export type GQLResolversTypes = ResolversObject<{ TagEdge: ResolverTypeWrapper< Omit & { node: GQLResolversTypes['Tag'] } > - TagEditorsInput: GQLTagEditorsInput TagOSS: ResolverTypeWrapper - TagSelectedInput: GQLTagSelectedInput TagsInput: GQLTagsInput TagsSort: GQLTagsSort ToggleCircleMemberInput: GQLToggleCircleMemberInput @@ -5267,12 +5045,9 @@ export type GQLResolversTypes = ResolversObject<{ UnvoteCommentInput: GQLUnvoteCommentInput UpdateArticleSensitiveInput: GQLUpdateArticleSensitiveInput UpdateArticleStateInput: GQLUpdateArticleStateInput - UpdateArticlesTagsInput: GQLUpdateArticlesTagsInput UpdateCampaignApplicationStateInput: GQLUpdateCampaignApplicationStateInput UpdateCommentsStateInput: GQLUpdateCommentsStateInput UpdateNotificationSettingInput: GQLUpdateNotificationSettingInput - UpdateTagSettingInput: GQLUpdateTagSettingInput - UpdateTagSettingType: GQLUpdateTagSettingType UpdateUserExtraInput: GQLUpdateUserExtraInput UpdateUserInfoInput: GQLUpdateUserInfoInput UpdateUserRoleInput: GQLUpdateUserRoleInput @@ -5317,7 +5092,6 @@ export type GQLResolversTypes = ResolversObject<{ UserInfoFields: GQLUserInfoFields UserInput: GQLUserInput UserLanguage: GQLUserLanguage - UserLoginInput: GQLUserLoginInput UserNotice: ResolverTypeWrapper UserNoticeType: GQLUserNoticeType UserOSS: ResolverTypeWrapper @@ -5340,7 +5114,6 @@ export type GQLResolversTypes = ResolversObject<{ } > UserRecommendationActivitySource: GQLUserRecommendationActivitySource - UserRegisterInput: GQLUserRegisterInput UserRestriction: ResolverTypeWrapper UserRestrictionType: GQLUserRestrictionType UserRole: GQLUserRole @@ -5368,7 +5141,6 @@ export type GQLResolversTypes = ResolversObject<{ /** Mapping between all available schema types and the resolvers parents */ export type GQLResolversParentTypes = ResolversObject<{ - AddArticlesTagsInput: GQLAddArticlesTagsInput AddCollectionsArticlesInput: GQLAddCollectionsArticlesInput AddCreditInput: GQLAddCreditInput AddCreditResult: Omit & { @@ -5467,7 +5239,6 @@ export type GQLResolversParentTypes = ResolversObject<{ CampaignStage: CampaignStageModel CampaignStageInput: GQLCampaignStageInput CampaignsInput: GQLCampaignsInput - ChangeEmailInput: GQLChangeEmailInput Circle: CircleModel CircleAnalytics: CircleModel CircleConnection: Omit & { @@ -5524,7 +5295,6 @@ export type GQLResolversParentTypes = ResolversObject<{ DatetimeRange: GQLDatetimeRange DatetimeRangeInput: GQLDatetimeRangeInput DeleteAnnouncementsInput: GQLDeleteAnnouncementsInput - DeleteArticlesTagsInput: GQLDeleteArticlesTagsInput DeleteCollectionArticlesInput: GQLDeleteCollectionArticlesInput DeleteCollectionsInput: GQLDeleteCollectionsInput DeleteCommentInput: GQLDeleteCommentInput @@ -5669,7 +5439,6 @@ export type GQLResolversParentTypes = ResolversObject<{ PutRemarkInput: GQLPutRemarkInput PutRestrictedUsersInput: GQLPutRestrictedUsersInput PutSkippedListItemInput: GQLPutSkippedListItemInput - PutTagInput: GQLPutTagInput PutWritingChallengeInput: GQLPutWritingChallengeInput Query: {} ReadArticleInput: GQLReadArticleInput @@ -5703,7 +5472,6 @@ export type GQLResolversParentTypes = ResolversObject<{ } ResetLikerIdInput: GQLResetLikerIdInput ResetPasswordInput: GQLResetPasswordInput - ResetWalletInput: GQLResetWalletInput Response: GQLResolversUnionTypes['Response'] ResponseConnection: GQLResponseConnection ResponseEdge: Omit & { @@ -5745,9 +5513,7 @@ export type GQLResolversParentTypes = ResolversObject<{ edges?: Maybe> } TagEdge: Omit & { node: GQLResolversParentTypes['Tag'] } - TagEditorsInput: GQLTagEditorsInput TagOSS: TagModel - TagSelectedInput: GQLTagSelectedInput TagsInput: GQLTagsInput ToggleCircleMemberInput: GQLToggleCircleMemberInput ToggleItemInput: GQLToggleItemInput @@ -5787,11 +5553,9 @@ export type GQLResolversParentTypes = ResolversObject<{ UnvoteCommentInput: GQLUnvoteCommentInput UpdateArticleSensitiveInput: GQLUpdateArticleSensitiveInput UpdateArticleStateInput: GQLUpdateArticleStateInput - UpdateArticlesTagsInput: GQLUpdateArticlesTagsInput UpdateCampaignApplicationStateInput: GQLUpdateCampaignApplicationStateInput UpdateCommentsStateInput: GQLUpdateCommentsStateInput UpdateNotificationSettingInput: GQLUpdateNotificationSettingInput - UpdateTagSettingInput: GQLUpdateTagSettingInput UpdateUserExtraInput: GQLUpdateUserExtraInput UpdateUserInfoInput: GQLUpdateUserInfoInput UpdateUserRoleInput: GQLUpdateUserRoleInput @@ -5833,7 +5597,6 @@ export type GQLResolversParentTypes = ResolversObject<{ } UserInfo: UserModel UserInput: GQLUserInput - UserLoginInput: GQLUserLoginInput UserNotice: NoticeItemModel UserOSS: UserModel UserPostMomentActivity: Omit< @@ -5854,7 +5617,6 @@ export type GQLResolversParentTypes = ResolversObject<{ UserRecommendationActivity: Omit & { nodes?: Maybe> } - UserRegisterInput: GQLUserRegisterInput UserRestriction: GQLUserRestriction UserSettings: UserModel UserStatus: UserModel @@ -6112,6 +5874,7 @@ export type GQLArticleResolvers< ParentType, ContextType > + bookmarked?: Resolver campaigns?: Resolver< Array, ParentType, @@ -6155,11 +5918,6 @@ export type GQLArticleResolvers< ContextType, RequireFields > - drafts?: Resolver< - Maybe>, - ParentType, - ContextType - > featuredComments?: Resolver< GQLResolversTypes['CommentConnection'], ParentType, @@ -6189,16 +5947,6 @@ export type GQLArticleResolvers< ContextType > mediaHash?: Resolver - newestPublishedDraft?: Resolver< - GQLResolversTypes['Draft'], - ParentType, - ContextType - > - newestUnpublishedDraft?: Resolver< - Maybe, - ParentType, - ContextType - > oss?: Resolver pinCommentLeft?: Resolver pinCommentLimit?: Resolver @@ -6265,14 +6013,7 @@ export type GQLArticleResolvers< shortHash?: Resolver slug?: Resolver state?: Resolver - sticky?: Resolver subscribed?: Resolver - subscribers?: Resolver< - GQLResolversTypes['UserConnection'], - ParentType, - ContextType, - RequireFields - > summary?: Resolver summaryCustomized?: Resolver< GQLResolversTypes['Boolean'], @@ -7675,7 +7416,6 @@ export type GQLLikerResolvers< ParentType, ContextType > - rateUSD?: Resolver, ParentType, ContextType> total?: Resolver __isTypeOf?: IsTypeOfResolverFn }> @@ -7778,12 +7518,6 @@ export type GQLMutationResolvers< ContextType = Context, ParentType extends GQLResolversParentTypes['Mutation'] = GQLResolversParentTypes['Mutation'] > = ResolversObject<{ - addArticlesTags?: Resolver< - GQLResolversTypes['Tag'], - ParentType, - ContextType, - RequireFields - > addBlockedSearchKeyword?: Resolver< GQLResolversTypes['BlockedSearchKeyword'], ParentType, @@ -7826,12 +7560,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - changeEmail?: Resolver< - GQLResolversTypes['User'], - ParentType, - ContextType, - RequireFields - > claimLogbooks?: Resolver< GQLResolversTypes['ClaimLogbooksResult'], ParentType, @@ -7867,12 +7595,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - deleteArticlesTags?: Resolver< - GQLResolversTypes['Tag'], - ParentType, - ContextType, - RequireFields - > deleteBlockedSearchKeywords?: Resolver< Maybe, ParentType, @@ -7933,7 +7655,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - generateLikerId?: Resolver generateSigningMessage?: Resolver< GQLResolversTypes['SigningMessageResult'], ParentType, @@ -8083,12 +7804,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - putTag?: Resolver< - GQLResolversTypes['Tag'], - ParentType, - ContextType, - RequireFields - > putWritingChallenge?: Resolver< GQLResolversTypes['WritingChallenge'], ParentType, @@ -8142,12 +7857,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - resetWallet?: Resolver< - GQLResolversTypes['User'], - ParentType, - ContextType, - RequireFields - > sendCampaignAnnouncement?: Resolver< Maybe, ParentType, @@ -8238,6 +7947,18 @@ export type GQLMutationResolvers< ContextType, RequireFields > + toggleBookmarkArticle?: Resolver< + GQLResolversTypes['Article'], + ParentType, + ContextType, + RequireFields + > + toggleBookmarkTag?: Resolver< + GQLResolversTypes['Tag'], + ParentType, + ContextType, + RequireFields + > toggleFollowCircle?: Resolver< GQLResolversTypes['Circle'], ParentType, @@ -8274,12 +7995,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - toggleTagRecommend?: Resolver< - GQLResolversTypes['Tag'], - ParentType, - ContextType, - RequireFields - > toggleUsersBadge?: Resolver< Array>, ParentType, @@ -8343,12 +8058,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - updateArticlesTags?: Resolver< - GQLResolversTypes['Tag'], - ParentType, - ContextType, - RequireFields - > updateCampaignApplicationState?: Resolver< GQLResolversTypes['Campaign'], ParentType, @@ -8367,12 +8076,6 @@ export type GQLMutationResolvers< ContextType, RequireFields > - updateTagSetting?: Resolver< - GQLResolversTypes['Tag'], - ParentType, - ContextType, - RequireFields - > updateUserExtra?: Resolver< GQLResolversTypes['User'], ParentType, @@ -8397,19 +8100,7 @@ export type GQLMutationResolvers< ContextType, RequireFields > - userLogin?: Resolver< - GQLResolversTypes['AuthResult'], - ParentType, - ContextType, - RequireFields - > userLogout?: Resolver - userRegister?: Resolver< - GQLResolversTypes['AuthResult'], - ParentType, - ContextType, - RequireFields - > verifyEmail?: Resolver< GQLResolversTypes['AuthResult'], ParentType, @@ -9068,12 +8759,6 @@ export type GQLRecommendationResolvers< ContextType, RequireFields > - readTagsArticles?: Resolver< - GQLResolversTypes['ArticleConnection'], - ParentType, - ContextType, - RequireFields - > selectedTags?: Resolver< GQLResolversTypes['TagConnection'], ParentType, @@ -9312,61 +8997,30 @@ export type GQLTagResolvers< RequireFields > content?: Resolver - cover?: Resolver, ParentType, ContextType> createdAt?: Resolver - creator?: Resolver, ParentType, ContextType> deleted?: Resolver - description?: Resolver< - Maybe, - ParentType, - ContextType - > - editors?: Resolver< - Maybe>, - ParentType, - ContextType, - Partial - > - followers?: Resolver< - GQLResolversTypes['UserConnection'], - ParentType, - ContextType, - RequireFields - > id?: Resolver isFollower?: Resolver< Maybe, ParentType, ContextType > - isOfficial?: Resolver< - Maybe, - ParentType, - ContextType - > numArticles?: Resolver numAuthors?: Resolver oss?: Resolver - owner?: Resolver, ParentType, ContextType> - participants?: Resolver< - GQLResolversTypes['UserConnection'], - ParentType, - ContextType, - RequireFields - > recommended?: Resolver< GQLResolversTypes['TagConnection'], ParentType, ContextType, RequireFields > - remark?: Resolver, ParentType, ContextType> - selected?: Resolver< - GQLResolversTypes['Boolean'], + recommendedAuthors?: Resolver< + GQLResolversTypes['UserConnection'], ParentType, ContextType, - RequireFields + RequireFields > + remark?: Resolver, ParentType, ContextType> __isTypeOf?: IsTypeOfResolverFn }> @@ -9399,7 +9053,6 @@ export type GQLTagOssResolvers< > = ResolversObject<{ boost?: Resolver score?: Resolver - selected?: Resolver __isTypeOf?: IsTypeOfResolverFn }> diff --git a/src/mutations/article/addArticlesTags.ts b/src/mutations/article/addArticlesTags.ts deleted file mode 100644 index 0da11724a..000000000 --- a/src/mutations/article/addArticlesTags.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import _difference from 'lodash/difference' -import _some from 'lodash/some' - -import { MAX_TAGS_PER_ARTICLE_LIMIT, USER_STATE } from 'common/enums' -import { environment } from 'common/environment' -import { - ForbiddenByStateError, - ForbiddenError, - TagNotFoundError, - TooManyTagsForArticleError, - UserInputError, -} from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['addArticlesTags'] = async ( - _, - { input: { id, articles, selected } }, - { viewer, dataSources: { atomService, articleService, tagService } } -) => { - if (!viewer.userName) { - throw new ForbiddenError('user has no username') - } - - if (viewer.state === USER_STATE.frozen) { - throw new ForbiddenByStateError(`${viewer.state} user has no permission`) - } - - if (!articles) { - throw new UserInputError('"articles" is required in update') - } - - const { id: dbId } = fromGlobalId(id) - const tag = await tagService.baseFindById(dbId) - if (!tag) { - throw new TagNotFoundError('tag not found') - } - - // add only allow: owner, editor, matty - const isOwner = tag.owner === viewer.id - const isEditor = _some(tag.editors, (editor) => editor === viewer.id) - const isMatty = viewer.id === environment.mattyId - const isMaintainer = isOwner || isEditor || isMatty - - if (!isMaintainer && selected) { - throw new ForbiddenError('not allow add tag to article') - } - - if (!isMatty && tag.id === environment.mattyChoiceTagId) { - throw new ForbiddenError('not allow to add official tag') - } - - // compare new and old article ids that have this tag (dedupe) - const oldIds = await tagService.findArticleIdsByTagIds([dbId]) - const newIds = articles.map((articleId) => fromGlobalId(articleId).id) - const addIds = _difference(newIds, oldIds) - - // not-maintainer can only add his/her own articles - if (!isMaintainer) { - const count = await atomService.count({ - table: 'article', - where: { authorId: viewer.id }, - whereIn: ['id', addIds], - }) - if (count !== addIds.length) { - throw new ForbiddenError('not allow add tag to article') - } - } - - // check article tags - for (const articleId of addIds) { - const tagIds = await articleService.findTagIds({ id: articleId }) - - if (tagIds.length > MAX_TAGS_PER_ARTICLE_LIMIT - 1) { - throw new TooManyTagsForArticleError( - `not allow more than ${MAX_TAGS_PER_ARTICLE_LIMIT} tags on article ${articleId}` - ) - } - } - - // add tag to articles - await tagService.createArticleTags({ - articleIds: addIds, - creator: viewer.id, - tagIds: [dbId], - selected, - }) - - return tag -} - -export default resolver diff --git a/src/mutations/article/deleteArticlesTags.ts b/src/mutations/article/deleteArticlesTags.ts deleted file mode 100644 index f06857157..000000000 --- a/src/mutations/article/deleteArticlesTags.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import _some from 'lodash/some' - -import { USER_STATE } from 'common/enums' -import { environment } from 'common/environment' -import { - AuthenticationError, - ForbiddenByStateError, - ForbiddenError, - TagNotFoundError, - UserInputError, -} from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['deleteArticlesTags'] = async ( - root, - { input: { id, articles } }, - { viewer, dataSources: { tagService } } -) => { - if (!viewer.id) { - throw new AuthenticationError('visitor has no permission') - } - - if (viewer.state === USER_STATE.frozen) { - throw new ForbiddenByStateError(`${viewer.state} user has no permission`) - } - - if (!articles) { - throw new UserInputError('"articles" is required in update') - } - - const { id: dbId } = fromGlobalId(id) - const tag = await tagService.baseFindById(dbId) - if (!tag) { - throw new TagNotFoundError('tag not found') - } - - // delete only allow: owner, editor, matty - const isOwner = tag.owner === viewer.id - const isEditor = _some(tag.editors, (editor) => editor === viewer.id) - const isMatty = viewer.id === environment.mattyId - const isMaintainer = isOwner || isEditor || isMatty - - if (!isMaintainer) { - throw new ForbiddenError('only editor, creator and matty can manage tag') - } - - // compare new and old article ids which have this tag - const deleteIds = articles.map((articleId) => fromGlobalId(articleId).id) - - // delete unwanted - await tagService.deleteArticleTagsByArticleIds({ - articleIds: deleteIds, - tagId: dbId, - }) - - return tag -} - -export default resolver diff --git a/src/mutations/article/editArticle.ts b/src/mutations/article/editArticle.ts index f8b6c8ca8..6134a71de 100644 --- a/src/mutations/article/editArticle.ts +++ b/src/mutations/article/editArticle.ts @@ -44,7 +44,6 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( input: { id, state, - sticky, pinned, tags, title, @@ -128,9 +127,9 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( } /** - * Pinned or Sticky + * Pinned */ - const isPinned = pinned ?? sticky + const isPinned = pinned if (typeof isPinned === 'boolean') { article = await articleService.updatePinned(article.id, viewer.id, isPinned) } diff --git a/src/mutations/article/index.ts b/src/mutations/article/index.ts index ce45be2b3..9bf6615c6 100644 --- a/src/mutations/article/index.ts +++ b/src/mutations/article/index.ts @@ -1,20 +1,14 @@ -import addArticlesTags from './addArticlesTags' import appreciateArticle from './appreciateArticle' -import deleteArticlesTags from './deleteArticlesTags' import deleteTags from './deleteTags' import editArticle from './editArticle' import mergeTags from './mergeTags' import publishArticle from './publishArticle' -import putTag from './putTag' import readArticle from './readArticle' import renameTag from './renameTag' import toggleArticleRecommend from './toggleArticleRecommend' -import toggleSubscribeArticle from './toggleSubscribeArticle' -import toggleTagRecommend from './toggleTagRecommend' +import toggleBookmarkArticle from './toggleBookmarkArticle' import updateArticleSensitive from './updateArticleSensitive' -import updateArticlesTags from './updateArticlesTags' import updateArticleState from './updateArticleState' -import updateTagSetting from './updateTagSetting' export default { Mutation: { @@ -23,17 +17,12 @@ export default { appreciateArticle, readArticle, toggleArticleRecommend, - toggleSubscribeArticle, + toggleBookmarkArticle, + toggleSubscribeArticle: toggleBookmarkArticle, updateArticleState, updateArticleSensitive, deleteTags, renameTag, mergeTags, - putTag, - addArticlesTags, - deleteArticlesTags, - updateArticlesTags, - updateTagSetting, - toggleTagRecommend, }, } diff --git a/src/mutations/article/mergeTags.ts b/src/mutations/article/mergeTags.ts index a36d3f2e8..219e1fa36 100644 --- a/src/mutations/article/mergeTags.ts +++ b/src/mutations/article/mergeTags.ts @@ -19,8 +19,6 @@ const resolver: GQLMutationResolvers['mergeTags'] = async ( tagIds, content, creator: environment.mattyId, - editors: [environment.mattyId], - owner: environment.mattyId, }) // invalidate extra nodes diff --git a/src/mutations/article/putTag.ts b/src/mutations/article/putTag.ts deleted file mode 100644 index 1882ba705..000000000 --- a/src/mutations/article/putTag.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import { - ASSET_TYPE, - MAX_TAG_CONTENT_LENGTH, - MAX_TAG_DESCRIPTION_LENGTH, - USER_STATE, -} from 'common/enums' -import { environment } from 'common/environment' -import { - AssetNotFoundError, - DuplicateTagError, - ForbiddenByStateError, - ForbiddenError, - NameInvalidError, - TagNotFoundError, - UserInputError, -} from 'common/errors' -import { - fromGlobalId, - normalizeTagInput, // stripAllPunct, -} from 'common/utils' - -const resolver: GQLMutationResolvers['putTag'] = async ( - _, - { input: { id, content, cover, description } }, - { viewer, dataSources: { systemService, tagService, atomService } } -) => { - if (!viewer.userName) { - throw new ForbiddenError('user has no username') - } - - if (viewer.state === USER_STATE.frozen) { - throw new ForbiddenByStateError(`${viewer.state} user has no permission`) - } - - // check if cover exists when receving parameter cover - let coverId - if (cover) { - const asset = await systemService.findAssetByUUID(cover) - if ( - !asset || - asset.type !== ASSET_TYPE.tagCover || - asset.authorId !== viewer.id - ) { - throw new AssetNotFoundError('tag cover asset does not exists') - } - coverId = asset.id - } else if (cover === null) { - coverId = null - } - - const tagContent = (content && normalizeTagInput(content)) || '' - - if (!tagContent || tagContent.length > MAX_TAG_CONTENT_LENGTH) { - throw new NameInvalidError( - `invalid tag name, either empty or too long (>${MAX_TAG_CONTENT_LENGTH})` - ) - } - if (description && description?.length > MAX_TAG_DESCRIPTION_LENGTH) { - throw new NameInvalidError( - `invalid too long tag description (>${MAX_TAG_DESCRIPTION_LENGTH})` - ) - } - - if (!id) { - // check if any same tag content exists - const tags = await tagService.findByContent({ content: tagContent }) - if (tags.length > 0) { - throw new DuplicateTagError(`dulpicate tag content: ${tagContent}`) - } - - const newTag = await tagService.create( - { - content: tagContent, - creator: viewer.id, - description, - editors: Array.from( - new Set( - environment.mattyId ? [environment.mattyId, viewer.id] : [viewer.id] - ) - ), - owner: viewer.id, - cover: coverId, - } - // ['id', 'content', 'description', 'creator', 'editors', 'owner', 'cover'] - ) - - return newTag - } else { - // update tag - const { id: dbId } = fromGlobalId(id) - const tag = await tagService.baseFindById(dbId) - if (!tag) { - throw new TagNotFoundError('tag not found') - } - - // update only allow: owner, editor, matty - const isOwner = tag.owner === viewer.id - const isEditor = !!tag.editors?.some((editor: any) => editor === viewer.id) - const isMatty = viewer.id === environment.mattyId - const isMaintainer = isOwner || isEditor || isMatty - - if (!isMaintainer) { - throw new ForbiddenError('only owner, editor, and matty can manage tag') - } - - // gather tag update params - const updateParams: { [key: string]: any } = {} - - if (tagContent) { - if (tagContent !== tag.content) { - const tags = await tagService.findByContent({ content: tagContent }) - if (tags.length > 0) { - throw new DuplicateTagError(`dulpicate tag content: ${tagContent}`) - } - } - updateParams.content = tagContent - } - if (typeof description !== 'undefined' && description !== null) { - updateParams.description = description - } - if (typeof coverId !== 'undefined') { - updateParams.cover = coverId - } - if (Object.keys(updateParams).length === 0) { - throw new UserInputError('bad request') - } - - const updateTag = await tagService.baseUpdate(dbId, updateParams) - - // delete unused tag cover - if (tag.cover && tag.cover !== updateTag.cover) { - const coverAsset = await atomService.findUnique({ - where: { id: tag.cover }, - table: 'asset', - }) - if (coverAsset) { - await systemService.deleteAssetAndAssetMap({ - [`${coverAsset.id}`]: coverAsset.path, - }) - } - } - return updateTag - } -} - -export default resolver diff --git a/src/mutations/article/toggleSubscribeArticle.ts b/src/mutations/article/toggleBookmarkArticle.ts similarity index 94% rename from src/mutations/article/toggleSubscribeArticle.ts rename to src/mutations/article/toggleBookmarkArticle.ts index f8c0fe151..4f48851ff 100644 --- a/src/mutations/article/toggleSubscribeArticle.ts +++ b/src/mutations/article/toggleBookmarkArticle.ts @@ -14,7 +14,7 @@ import { } from 'common/errors' import { fromGlobalId } from 'common/utils' -const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( +const resolver: GQLMutationResolvers['toggleBookmarkArticle'] = async ( _, { input: { id, enabled } }, { viewer, dataSources: { atomService, articleService, notificationService } } @@ -29,7 +29,7 @@ const resolver: GQLMutationResolvers['toggleSubscribeArticle'] = async ( } const { id: dbId } = fromGlobalId(id) - // banned and archived articles shall still be able to be unsubscribed + // banned and archived articles shall still be able to be unbookmarked const article = enabled === false ? await atomService.findFirst({ diff --git a/src/mutations/article/toggleTagRecommend.ts b/src/mutations/article/toggleTagRecommend.ts deleted file mode 100644 index af29d00e1..000000000 --- a/src/mutations/article/toggleTagRecommend.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import { TagNotFoundError } from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['toggleTagRecommend'] = async ( - _, - { input: { id, enabled } }, - { dataSources: { tagService, atomService } } -) => { - const { id: dbId } = fromGlobalId(id) - const tag = await atomService.tagIdLoader.load(dbId) - if (!tag) { - throw new TagNotFoundError('target tag does not exists') - } - - await (enabled - ? tagService.addTagRecommendation - : tagService.removeTagRecommendation)(dbId) - - return tag -} - -export default resolver diff --git a/src/mutations/article/updateArticlesTags.ts b/src/mutations/article/updateArticlesTags.ts deleted file mode 100644 index 31624f203..000000000 --- a/src/mutations/article/updateArticlesTags.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import _some from 'lodash/some' - -import { USER_STATE } from 'common/enums' -import { environment } from 'common/environment' -import { - AuthenticationError, - ForbiddenByStateError, - ForbiddenError, - TagNotFoundError, - UserInputError, -} from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['updateArticlesTags'] = async ( - root, - { input: { id, articles, isSelected } }, - { viewer, dataSources: { tagService } } -) => { - if (!viewer.id) { - throw new AuthenticationError('visitor has no permission') - } - - if (viewer.state === USER_STATE.frozen) { - throw new ForbiddenByStateError(`${viewer.state} user has no permission`) - } - - if (!articles) { - throw new UserInputError('"articles" is required in update') - } - - const { id: dbId } = fromGlobalId(id) - const tag = await tagService.baseFindById(dbId) - if (!tag) { - throw new TagNotFoundError('tag not found') - } - - // update only allow: owner, editor, matty - const isOwner = tag.owner === viewer.id - const isEditor = _some(tag.editors, (editor) => editor === viewer.id) - const isMatty = viewer.id === environment.mattyId - const isMaintainer = isOwner || isEditor || isMatty - - if (!isMaintainer) { - throw new ForbiddenError('only owner, editor and matty can manage tag') - } - - // set article as selected or not - const { id: articleId } = fromGlobalId(articles[0]) - await tagService.putArticleTag({ - articleId, - tagId: dbId, - data: { selected: isSelected }, - }) - - return tag -} - -export default resolver diff --git a/src/mutations/article/updateTagSetting.ts b/src/mutations/article/updateTagSetting.ts deleted file mode 100644 index 4f8ae9dc9..000000000 --- a/src/mutations/article/updateTagSetting.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { GQLMutationResolvers, Tag } from 'definitions' - -import _difference from 'lodash/difference' -import _some from 'lodash/some' -import _uniq from 'lodash/uniq' - -import { - CACHE_KEYWORD, - NODE_TYPES, - USER_STATE, - UPDATE_TAG_SETTING_TYPE, -} from 'common/enums' -import { environment } from 'common/environment' -import { - AuthenticationError, - ForbiddenByStateError, - ForbiddenError, - TagEditorsReachLimitError, - TagNotFoundError, - UserInputError, -} from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['updateTagSetting'] = async ( - _, - { input: { id, type, editors } }, - { viewer, dataSources: { systemService, tagService } } -) => { - if (!viewer.id) { - throw new AuthenticationError('viewer has no permission') - } - - if (viewer.state === USER_STATE.frozen) { - throw new ForbiddenByStateError(`${viewer.state} user has no permission`) - } - - const { id: tagId } = fromGlobalId(id) - const tag = await tagService.baseFindById(tagId) - - if (!tag) { - throw new TagNotFoundError('tag not found') - } - - const { mattyId } = environment - const isOwner = tag.owner === viewer.id - const isMatty = viewer.id === mattyId - - let updatedTag - - switch (type) { - case UPDATE_TAG_SETTING_TYPE.adopt: { - // check feature is enabled - const feature = await systemService.getFeatureFlag('tag_adoption') - if ( - feature && - !(await systemService.isFeatureEnabled(feature.flag, viewer)) - ) { - throw new ForbiddenError('viewer has no permission') - } - - // if tag has been adopted, throw error - if (tag.owner) { - throw new ForbiddenError('viewer has no permission') - } - - // update - updatedTag = await tagService.baseUpdate(tagId, { - owner: viewer.id, - editors: _uniq([...(tag.editors ?? []), viewer.id]), - }) - - break - } - case UPDATE_TAG_SETTING_TYPE.leave: { - // if tag has no owner or owner is not viewer, throw error - if (!tag.owner || (tag.owner && !isOwner)) { - throw new ForbiddenError('viewer has no permission') - } - - // remove viewer from editors - const newEditors = isMatty - ? undefined - : (tag.editors || []).filter((item: string) => item !== viewer.id) - - // update - updatedTag = await tagService.baseUpdate(tagId, { - owner: null, - editors: newEditors, - }) - - break - } - case UPDATE_TAG_SETTING_TYPE.add_editor: { - // only owner can add editors - if (!isOwner) { - throw new ForbiddenError('viewer has no permission') - } - if (!editors || editors.length === 0) { - throw new UserInputError('editors are invalid') - } - - // gather valid editors - const newEditors = - (editors - .map((editor) => { - const { id: editorId } = fromGlobalId(editor) - if (!(tag.editors ?? []).includes(editorId)) { - return editorId - } - }) - .filter((editorId) => editorId !== undefined) as string[]) || [] - - // editors composed by 4 editors, matty and owner - const dedupedEditors = _uniq([...(tag.editors ?? []), ...newEditors]) - if (dedupedEditors.length > 6) { - throw new TagEditorsReachLimitError('number of editors reaches limit') - } - - // update - updatedTag = await tagService.baseUpdate(tagId, { - editors: dedupedEditors, - }) - - break - } - case UPDATE_TAG_SETTING_TYPE.remove_editor: { - // only owner can remove editors - if (!isOwner) { - throw new ForbiddenError('viewer has no permission') - } - if (!editors || editors.length === 0) { - throw new UserInputError('editors are invalid') - } - - // gather valid editors - const removeEditors = - (editors - .map((editor) => { - const { id: editorId } = fromGlobalId(editor) - if (editorId === tag.owner || editorId === mattyId) { - return - } - return editorId - }) - .filter((editorId) => editorId !== undefined) as string[]) || [] - - // update - updatedTag = await tagService.baseUpdate(tagId, { - editors: _difference(tag.editors, removeEditors), - }) - break - } - case UPDATE_TAG_SETTING_TYPE.leave_editor: { - const isEditor = _some(tag.editors, (editor) => editor === viewer.id) - if (!isEditor) { - throw new ForbiddenError('viewer has no permission') - } - if (isOwner || isMatty) { - throw new ForbiddenError('viewer cannot leave') - } - - // update - updatedTag = await tagService.baseUpdate(tagId, { - editors: _difference(tag.editors, [viewer.id]), - }) - - break - } - default: { - throw new UserInputError('unknown update tag type') - } - } - - if (updatedTag) { - // invalidate extra nodes - ;(updatedTag as Tag & { [CACHE_KEYWORD]: any })[CACHE_KEYWORD] = [ - { id: viewer.id, type: NODE_TYPES.User }, - ] - } - return updatedTag -} - -export default resolver diff --git a/src/mutations/user/changeEmail.ts b/src/mutations/user/changeEmail.ts deleted file mode 100644 index 54efc8f04..000000000 --- a/src/mutations/user/changeEmail.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import { VERIFICATION_CODE_STATUS, VERIFICATION_CODE_TYPE } from 'common/enums' -import { - CodeExpiredError, - CodeInactiveError, - CodeInvalidError, - EmailExistsError, - UserNotFoundError, -} from 'common/errors' - -const resolver: GQLMutationResolvers['changeEmail'] = async ( - _, - { - input: { - oldEmail: rawOldEmail, - oldEmailCodeId, - newEmail: rawNewEmail, - newEmailCodeId, - }, - }, - { dataSources: { userService, atomService } } -) => { - const oldEmail = rawOldEmail ? rawOldEmail.toLowerCase() : null - const newEmail = rawNewEmail ? rawNewEmail.toLowerCase() : null - - const [[oldCode], [newCode]] = await Promise.all([ - userService.findVerificationCodes({ - where: { - uuid: oldEmailCodeId, - email: oldEmail, - type: VERIFICATION_CODE_TYPE.email_reset, - }, - }), - userService.findVerificationCodes({ - where: { - uuid: newEmailCodeId, - email: newEmail, - type: VERIFICATION_CODE_TYPE.email_reset_confirm, - }, - }), - ]) - - // check codes - const isOldCodeVerified = oldCode.status === VERIFICATION_CODE_STATUS.verified - const isNewCodeVerified = newCode.status === VERIFICATION_CODE_STATUS.verified - const hasExpiredCode = - oldCode.status === VERIFICATION_CODE_STATUS.expired || - newCode.status === VERIFICATION_CODE_STATUS.expired - const hasInactiveCode = - oldCode.status === VERIFICATION_CODE_STATUS.inactive || - newCode.status === VERIFICATION_CODE_STATUS.inactive - - if (hasExpiredCode) { - throw new CodeExpiredError('code is expired') - } - if (hasInactiveCode) { - throw new CodeInactiveError('code is retired') - } - if (!isOldCodeVerified || !isNewCodeVerified) { - throw new CodeInvalidError('code does not exists') - } - - // check email - const user = await userService.findByEmail(oldCode.email) - if (!user) { - throw new UserNotFoundError('target user does not exists') - } - - // check new email - const isNewEmailExisted = await userService.findByEmail(newCode.email) - if (isNewEmailExisted) { - throw new EmailExistsError('email already exists') - } - - const newUser = await atomService.update({ - table: 'user', - where: { id: user.id }, - data: { - email: newCode.email, - }, - }) - - // mark code status as used - await userService.markVerificationCodeAs({ - codeId: oldCode.id, - status: VERIFICATION_CODE_STATUS.used, - }) - await userService.markVerificationCodeAs({ - codeId: newCode.id, - status: VERIFICATION_CODE_STATUS.used, - }) - - return newUser -} - -export default resolver diff --git a/src/mutations/user/generateLikerId.ts b/src/mutations/user/generateLikerId.ts deleted file mode 100644 index 3cbc28aec..000000000 --- a/src/mutations/user/generateLikerId.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import { ForbiddenError } from 'common/errors' - -const resolver: GQLMutationResolvers['generateLikerId'] = async ( - _, - __, - { viewer, dataSources: { userService } } -) => { - if (!viewer.userName) { - throw new ForbiddenError('user has no username') - } - - const { ip } = viewer - - const liker = await userService.findLiker({ userId: viewer.id }) - - // generate - if (!liker || !liker.likerId) { - await userService.registerLikerId({ - userId: viewer.id, - userName: viewer.userName, - ip, - }) - } - - // claim - else { - if (liker.accountType === 'temporal') { - await userService.claimLikerId({ - userId: viewer.id, - liker, - ip, - }) - } - } - - return userService.baseFindById(viewer.id) -} - -export default resolver diff --git a/src/mutations/user/index.ts b/src/mutations/user/index.ts index bc5bb7d05..8767f29ca 100644 --- a/src/mutations/user/index.ts +++ b/src/mutations/user/index.ts @@ -1,12 +1,10 @@ import addCredit from './addCredit' -import changeEmail from './changeEmail' import claimLogbooks from './claimLogbooks' import clearReadHistory from './clearReadHistory' import clearSearchHistory from './clearSearchHistory' import confirmVerificationCode from './confirmVerificationCode' import connectStripeAccount from './connectStripeAccount' import emailLogin from './emailLogin' -import generateLikerId from './generateLikerId' import generateSigningMessage from './generateSigningMessage' import migration from './migration' import payout from './payout' @@ -15,7 +13,6 @@ import putFeaturedTags from './putFeaturedTags' import refreshIPNSFeed from './refreshIPNSFeed' import resetLikerId from './resetLikerId' import resetPassword from './resetPassword' -import resetWallet from './resetWallet' import sendVerificationCode from './sendVerificationCode' import setCurrency from './setCurrency' import setEmail from './setEmail' @@ -23,7 +20,7 @@ import setPassword from './setPassword' import setUserName from './setUserName' import { socialLogin, addSocialLogin, removeSocialLogin } from './socialLogin' import toggleBlockUser from './toggleBlockUser' -import toggleFollowTag from './toggleFollowTag' +import toggleBookmarkTag from './toggleBookmarkTag' import toggleFollowUser from './toggleFollowUser' import toggleUsersBadge from './toggleUsersBadge' import unbindLikerId from './unbindLikerId' @@ -32,9 +29,7 @@ import updateUserExtra from './updateUserExtra' import updateUserInfo from './updateUserInfo' import updateUserRole from './updateUserRole' import updateUserState from './updateUserState' -import userLogin from './userLogin' import userLogout from './userLogout' -import userRegister from './userRegister' import verifyEmail from './verifyEmail' import { walletLogin, addWalletLogin, removeWalletLogin } from './walletLogin' @@ -43,23 +38,19 @@ export default { sendVerificationCode, confirmVerificationCode, resetPassword, - changeEmail, - userRegister, - userLogin, emailLogin, userLogout, walletLogin, addWalletLogin, removeWalletLogin, - resetWallet, - generateLikerId, generateSigningMessage, resetLikerId, updateUserInfo, updateNotificationSetting, setCurrency, toggleBlockUser, - toggleFollowTag, + toggleBookmarkTag, + toggleFollowTag: toggleBookmarkTag, toggleFollowUser, clearReadHistory, clearSearchHistory, diff --git a/src/mutations/user/resetWallet.ts b/src/mutations/user/resetWallet.ts deleted file mode 100644 index ad8f4819b..000000000 --- a/src/mutations/user/resetWallet.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { GQLMutationResolvers } from 'definitions' - -import { ForbiddenError } from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLMutationResolvers['resetWallet'] = async ( - _, - { input: { id } }, - { dataSources: { atomService, userService } } -) => { - const { id: dbId } = fromGlobalId(id) - const user = await atomService.userIdLoader.load(dbId) - - if (!user || !user.ethAddress) { - throw new ForbiddenError("user doesn't exist or have a crypto wallet") - } - - if (!user.passwordHash) { - throw new ForbiddenError( - 'user registered with crypto wallet is not allowed' - ) - } - - const updatedUser = await atomService.update({ - table: 'user', - where: { id: user.id }, - data: { updatedAt: new Date(), ethAddress: null }, - }) - - return updatedUser -} - -export default resolver diff --git a/src/mutations/user/sendVerificationCode.ts b/src/mutations/user/sendVerificationCode.ts index af4ab7953..76e6a66c9 100644 --- a/src/mutations/user/sendVerificationCode.ts +++ b/src/mutations/user/sendVerificationCode.ts @@ -75,8 +75,6 @@ const resolver: GQLMutationResolvers['sendVerificationCode'] = async ( if ( type === VERIFICATION_CODE_TYPE.payment_password_reset || - type === VERIFICATION_CODE_TYPE.password_reset || - type === VERIFICATION_CODE_TYPE.email_reset || type === VERIFICATION_CODE_TYPE.email_verify ) { if (!user) { diff --git a/src/mutations/user/toggleFollowTag.ts b/src/mutations/user/toggleBookmarkTag.ts similarity index 94% rename from src/mutations/user/toggleFollowTag.ts rename to src/mutations/user/toggleBookmarkTag.ts index d71608f29..11d8a8d07 100644 --- a/src/mutations/user/toggleFollowTag.ts +++ b/src/mutations/user/toggleBookmarkTag.ts @@ -4,7 +4,7 @@ import { CACHE_KEYWORD, NODE_TYPES, TAG_ACTION } from 'common/enums' import { ForbiddenError, TagNotFoundError } from 'common/errors' import { fromGlobalId } from 'common/utils' -const resolver: GQLMutationResolvers['toggleFollowTag'] = async ( +const resolver: GQLMutationResolvers['toggleBookmarkTag'] = async ( _, { input: { id, enabled } }, { viewer, dataSources: { tagService, atomService } } diff --git a/src/mutations/user/updateUserInfo.ts b/src/mutations/user/updateUserInfo.ts index 00d7c178c..4c3b5f7bd 100644 --- a/src/mutations/user/updateUserInfo.ts +++ b/src/mutations/user/updateUserInfo.ts @@ -8,9 +8,6 @@ import { AssetNotFoundError, AuthenticationError, DisplayNameInvalidError, - ForbiddenError, - NameExistsError, - NameInvalidError, PasswordInvalidError, UserInputError, } from 'common/errors' @@ -19,7 +16,6 @@ import { generatePasswordhash, isValidDisplayName, isValidPaymentPassword, - isValidUserName, setCookie, } from 'common/utils' import { cfsvc } from 'connectors' @@ -31,12 +27,7 @@ const resolver: GQLMutationResolvers['updateUserInfo'] = async ( { input }, { viewer, - dataSources: { - userService, - systemService, - notificationService, - atomService, - }, + dataSources: { systemService, notificationService, atomService }, req, res, } @@ -126,52 +117,6 @@ const resolver: GQLMutationResolvers['updateUserInfo'] = async ( updateParams.profileCover = null } - // check user name is editable - if (input.userName) { - const isUserNameEditable = await userService.isUserNameEditable(viewer.id) - if (!isUserNameEditable) { - auditLog({ - actorId: viewer.id, - action: AUDIT_LOG_ACTION.updateUsername, - oldValue: viewer.userName, - newValue: input.userName, - status: AUDIT_LOG_STATUS.failed, - remark: 'user name is not allow to edit', - }) - throw new ForbiddenError('userName is not allow to edit') - } - if (!isValidUserName(input.userName.toLowerCase())) { - auditLog({ - actorId: viewer.id, - action: AUDIT_LOG_ACTION.updateUsername, - oldValue: viewer.userName, - newValue: input.userName, - status: AUDIT_LOG_STATUS.failed, - remark: 'invalid user name', - }) - throw new NameInvalidError('invalid user name') - } - - // allows user to set the same userName - const isSameUserName = - viewer.userName.toLowerCase() === input.userName.toLowerCase() - const isUserNameExists = await userService.checkUserNameExists( - input.userName - ) - if (!isSameUserName && isUserNameExists) { - auditLog({ - actorId: viewer.id, - action: AUDIT_LOG_ACTION.updateUsername, - oldValue: viewer.userName, - newValue: input.userName, - status: AUDIT_LOG_STATUS.failed, - remark: 'user name already exists', - }) - throw new NameExistsError('user name already exists') - } - updateParams.userName = input.userName.toLowerCase() - } - // check user display name if (input.displayName) { if (!isValidDisplayName(input.displayName) && !viewer.hasRole('admin')) { @@ -233,24 +178,6 @@ const resolver: GQLMutationResolvers['updateUserInfo'] = async ( }) logger.info(`Updated id ${viewer.id} in "user"`) - // add user name edit history - if (input.userName) { - await atomService.create({ - table: 'username_edit_history', - data: { - userId: viewer.id, - previous: viewer.userName, - }, - }) - auditLog({ - actorId: viewer.id, - action: AUDIT_LOG_ACTION.updateUsername, - oldValue: viewer.userName, - newValue: input.userName, - status: AUDIT_LOG_STATUS.succeeded, - }) - } - if (input.displayName && viewer.displayName !== input.displayName) { auditLog({ actorId: viewer.id, diff --git a/src/mutations/user/userLogin.ts b/src/mutations/user/userLogin.ts deleted file mode 100644 index 86ad06cd2..000000000 --- a/src/mutations/user/userLogin.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { AuthMode, GQLMutationResolvers } from 'definitions' - -import { AUTH_RESULT_TYPE } from 'common/enums' -import { getViewerFromUser, setCookie } from 'common/utils' - -const resolver: GQLMutationResolvers['userLogin'] = async ( - _, - { input: { email: rawEmail, password } }, - context -) => { - const { - dataSources: { userService, systemService }, - req, - res, - } = context - - const email = rawEmail.toLowerCase() - const archivedCallback = async () => - systemService.saveAgentHash(context.viewer.agentHash || '', email) - const { token, user } = await userService.loginByEmail({ - email, - password, - archivedCallback, - }) - await userService.verifyPassword({ password, hash: user.passwordHash ?? '' }) - - setCookie({ req, res, token, user }) - - context.viewer = await getViewerFromUser(user) - context.viewer.authMode = user.role as AuthMode - context.viewer.scope = {} - - return { - token, - auth: true, - type: AUTH_RESULT_TYPE.Login, - user, - } -} - -export default resolver diff --git a/src/mutations/user/userRegister.ts b/src/mutations/user/userRegister.ts deleted file mode 100644 index e92de4a4d..000000000 --- a/src/mutations/user/userRegister.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { AuthMode, GQLMutationResolvers } from 'definitions' - -import { - VERIFICATION_CODE_STATUS, - VERIFICATION_CODE_TYPE, - AUTH_RESULT_TYPE, -} from 'common/enums' -import { - CodeExpiredError, - CodeInactiveError, - CodeInvalidError, - DisplayNameInvalidError, - EmailExistsError, - EmailInvalidError, - NameExistsError, - NameInvalidError, - PasswordInvalidError, -} from 'common/errors' -import { - getViewerFromUser, - isValidDisplayName, - isValidEmail, - isValidPassword, - isValidUserName, - setCookie, -} from 'common/utils' - -const resolver: GQLMutationResolvers['userRegister'] = async ( - _, - { input }, - context -) => { - const { - dataSources: { userService }, - req, - res, - } = context - const { email: rawEmail, userName, displayName, password, codeId } = input - const email = rawEmail.toLowerCase() - if (!isValidEmail(email, { allowPlusSign: false })) { - throw new EmailInvalidError('invalid email address format') - } - - // check verification code - const codes = await userService.findVerificationCodes({ - where: { - uuid: codeId, - email, - type: VERIFICATION_CODE_TYPE.register, - }, - }) - const code = codes?.length > 0 ? codes[0] : {} - - // check code - if (code.status === VERIFICATION_CODE_STATUS.expired) { - throw new CodeExpiredError('code is expired') - } - if (code.status === VERIFICATION_CODE_STATUS.inactive) { - throw new CodeInactiveError('code is retired') - } - if (code.status !== VERIFICATION_CODE_STATUS.verified) { - throw new CodeInvalidError('code does not exists') - } - - // check email - const otherUser = await userService.findByEmail(email) - if (otherUser) { - throw new EmailExistsError('email address has already been registered') - } - - // check display name - // Note: We will use "userName" to pre-fill "displayName" in step-1 of signUp flow on website - const shouldCheckDisplayName = displayName !== userName - if (shouldCheckDisplayName && !isValidDisplayName(displayName)) { - throw new DisplayNameInvalidError('invalid user display name') - } - - // check password - if (!isValidPassword(password)) { - throw new PasswordInvalidError('invalid user password') - } - - let newUserName - if (userName) { - if (!isValidUserName(userName.toLowerCase())) { - throw new NameInvalidError('invalid user name') - } - - if (await userService.checkUserNameExists(userName)) { - throw new NameExistsError('user name already exists') - } - - newUserName = userName - } else { - newUserName = await userService.generateUserName(email) - } - - const newUser = await userService.create({ - ...input, - email, - emailVerified: true, - userName: newUserName.toLowerCase(), - }) - // mark code status as used - await userService.markVerificationCodeAs({ - codeId: code.id, - status: VERIFICATION_CODE_STATUS.used, - }) - await userService.postRegister(newUser) - - const { token, user } = await userService.loginByEmail({ ...input, email }) - - setCookie({ req, res, token, user }) - - context.viewer = await getViewerFromUser(user) - context.viewer.authMode = user.role as AuthMode - context.viewer.scope = {} - - return { - token, - auth: true, - type: AUTH_RESULT_TYPE.Signup, - user, - } -} - -export default resolver diff --git a/src/mutations/user/walletLogin.ts b/src/mutations/user/walletLogin.ts index ef1d7d0e3..92fa53b88 100644 --- a/src/mutations/user/walletLogin.ts +++ b/src/mutations/user/walletLogin.ts @@ -8,21 +8,12 @@ import type { import { Hex } from 'viem' import { - VERIFICATION_CODE_STATUS, - VERIFICATION_CODE_TYPE, AUTH_RESULT_TYPE, SIGNING_MESSAGE_PURPOSE, AUDIT_LOG_ACTION, AUDIT_LOG_STATUS, } from 'common/enums' -import { - CodeExpiredError, - CodeInactiveError, - CodeInvalidError, - EmailExistsError, - EthAddressNotFoundError, - UserInputError, -} from 'common/errors' +import { EthAddressNotFoundError, UserInputError } from 'common/errors' import { auditLog } from 'common/logger' import { getViewerFromUser, setCookie } from 'common/utils' @@ -68,18 +59,7 @@ const _walletLogin: Exclude< undefined > = async ( _, - { - input: { - ethAddress, - nonce, - signedMessage, - signature, - email, - codeId, - language, - referralCode, - }, - }, + { input: { ethAddress, nonce, signedMessage, signature, language } }, context ) => { const { @@ -144,59 +124,11 @@ const _walletLogin: Exclude< } } else { // signup - if (email) { - if (!codeId) { - throw new UserInputError('email and codeId are required') - } - // check verification code - const codes = await userService.findVerificationCodes({ - where: { - uuid: codeId, - email, - type: VERIFICATION_CODE_TYPE.register, - }, - }) - const code = codes?.length > 0 ? codes[0] : {} - - // check code - if (code.status === VERIFICATION_CODE_STATUS.expired) { - throw new CodeExpiredError('code is expired') - } - if (code.status === VERIFICATION_CODE_STATUS.inactive) { - throw new CodeInactiveError('code is retired') - } - if (code.status !== VERIFICATION_CODE_STATUS.verified) { - throw new CodeInvalidError('code does not exists') - } - - // check email - const otherUser = await userService.findByEmail(email) - if (otherUser) { - throw new EmailExistsError('email address has already been registered') - } - - const userName = await userService.generateUserName(email) - user = await userService.create({ - email, - userName, - displayName: userName, - ethAddress: ethAddress.toLowerCase(), // save the lower case ones - language: language || viewer.language, - referralCode, - }) - // mark code status as used - await userService.postRegister(user) - await userService.markVerificationCodeAs({ - codeId: code.id, - status: VERIFICATION_CODE_STATUS.used, - }) - } else { - user = await userService.create({ - ethAddress: ethAddress.toLowerCase(), - language: language || viewer.language, - }) - await userService.postRegister(user) - } + user = await userService.create({ + ethAddress: ethAddress.toLowerCase(), + language: language || viewer.language, + }) + await userService.postRegister(user) } return tryLogin(AUTH_RESULT_TYPE.Signup, user) } diff --git a/src/queries/article/subscribed.ts b/src/queries/article/bookmarked.ts similarity index 73% rename from src/queries/article/subscribed.ts rename to src/queries/article/bookmarked.ts index 15bc220af..39837e2b6 100644 --- a/src/queries/article/subscribed.ts +++ b/src/queries/article/bookmarked.ts @@ -2,7 +2,7 @@ import type { GQLArticleResolvers } from 'definitions' import { USER_ACTION } from 'common/enums' -const resolver: GQLArticleResolvers['subscribed'] = async ( +const resolver: GQLArticleResolvers['bookmarked'] = async ( { id: articleId }, _, { viewer, dataSources: { atomService } } @@ -11,7 +11,7 @@ const resolver: GQLArticleResolvers['subscribed'] = async ( return false } - const subscribedCount = await atomService.count({ + const bookmarkedCount = await atomService.count({ table: 'action_article', where: { userId: viewer.id, @@ -20,7 +20,7 @@ const resolver: GQLArticleResolvers['subscribed'] = async ( }, }) - return subscribedCount > 0 + return bookmarkedCount > 0 } export default resolver diff --git a/src/queries/article/index.ts b/src/queries/article/index.ts index eac295715..9dd6ad6b5 100644 --- a/src/queries/article/index.ts +++ b/src/queries/article/index.ts @@ -10,6 +10,7 @@ import appreciationsReceivedTotal from './appreciationsReceivedTotal' import assets from './assets' import author from './author' import availableTranslations from './availableTranslations' +import bookmarked from './bookmarked' import campaigns from './campaigns' import canComment from './canComment' import canSuperLike from './canSuperLike' @@ -46,26 +47,16 @@ import sensitiveByAuthor from './sensitiveByAuthor' import shortHash from './shortHash' import slug from './slug' import state from './state' -import sticky from './sticky' -import subscribed from './subscribed' -import subscribers from './subscribers' import summary from './summary' import summaryCustomized from './summaryCustomized' import tagArticles from './tag/articles' import tagArticlesExcludeSpam from './tag/articlesExcludeSpam' -import tagCover from './tag/cover' -import tagCreator from './tag/creator' -import tagEditors from './tag/editors' -import tagFollowers from './tag/followers' import tagIsFollower from './tag/isFollower' -import tagIsOfficial from './tag/isOfficial' import tagNumArticles from './tag/numArticles' import tagNumAuthors from './tag/numAuthors' import * as tagOSS from './tag/oss' -import tagOwner from './tag/owner' -import tagParticipants from './tag/participants' import tagsRecommended from './tag/recommended' -import tagSelected from './tag/selected' +import tagsRecommendedAuthors from './tag/recommendedAuthors' import tags from './tags' import title from './title' import transactionsReceivedBy from './transactionsReceivedBy' @@ -112,10 +103,9 @@ const schema: GQLResolvers = { mediaHash, shortHash, state, - sticky, pinned, - subscribed, - subscribers, + subscribed: bookmarked, + bookmarked, tags, translation: articleTranslation, availableTranslations, @@ -143,19 +133,12 @@ const schema: GQLResolvers = { id: ({ id }) => toGlobalId({ type: NODE_TYPES.Tag, id }), articles: tagArticles, articlesExcludeSpam: tagArticlesExcludeSpam, - selected: tagSelected, - creator: tagCreator, - editors: tagEditors, - owner: tagOwner, isFollower: tagIsFollower, - isOfficial: tagIsOfficial, numArticles: tagNumArticles, numAuthors: tagNumAuthors, - followers: tagFollowers, oss: (root) => root, - cover: tagCover, - participants: tagParticipants, recommended: tagsRecommended, + recommendedAuthors: tagsRecommendedAuthors, }, ArticleVersion: { id: ({ id }) => toGlobalId({ type: NODE_TYPES.ArticleVersion, id }), @@ -183,7 +166,6 @@ const schema: GQLResolvers = { TagOSS: { boost: tagOSS.boost, score: tagOSS.score, - selected: tagOSS.selected, }, } diff --git a/src/queries/article/sticky.ts b/src/queries/article/sticky.ts deleted file mode 100644 index d293b39da..000000000 --- a/src/queries/article/sticky.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { GQLArticleResolvers } from 'definitions' - -const resolver: GQLArticleResolvers['sticky'] = async ({ pinned }) => pinned - -export default resolver diff --git a/src/queries/article/subscribers.ts b/src/queries/article/subscribers.ts deleted file mode 100644 index 875e6f620..000000000 --- a/src/queries/article/subscribers.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { GQLArticleResolvers } from 'definitions' - -import { USER_ACTION } from 'common/enums' -import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' - -const resolver: GQLArticleResolvers['subscribers'] = async ( - { id: articleId }, - { input }, - { - dataSources: { - articleService, - atomService, - connections: { knex }, - }, - } -) => { - const { take, skip } = fromConnectionArgs(input) - - const [countRecord, actions] = await Promise.all([ - knex('action_article') - .where({ targetId: articleId, action: USER_ACTION.subscribe }) - .countDistinct('user_id') - .first(), - articleService.findSubscriptions({ id: articleId, skip, take }), - ]) - - const totalCount = parseInt( - countRecord ? (countRecord.count as string) : '0', - 10 - ) - - return connectionFromPromisedArray( - atomService.userIdLoader.loadMany( - actions.map(({ userId }: { userId: string }) => userId) - ), - input, - totalCount - ) -} - -export default resolver diff --git a/src/queries/article/tag/articles.ts b/src/queries/article/tag/articles.ts index a6d0a1dbd..fe79d7843 100644 --- a/src/queries/article/tag/articles.ts +++ b/src/queries/article/tag/articles.ts @@ -7,27 +7,26 @@ const resolver: GQLTagResolvers['articles'] = async ( { input }, { dataSources: { tagService, atomService } } ) => { - const { selected, sortBy } = input + const { sortBy } = input const { take, skip } = fromConnectionArgs(input) - - const isFromRecommendation = - ((root as any).numArticles || (root as any).numAuthors) > 0 + const isHottest = sortBy === 'byHottestDesc' const [totalCount, articleIds] = await Promise.all([ - tagService.countArticles({ - id: root.id, - selected, - withSynonyms: isFromRecommendation, - }), - tagService.findArticleIds({ - id: root.id, - selected, - sortBy: sortBy as 'byHottestDesc' | 'byCreatedAtDesc' | undefined, - withSynonyms: isFromRecommendation, - // excludeSpam: true, - skip, - take, - }), + isHottest + ? tagService.countHottestArticles({ id: root.id }) + : tagService.countArticles({ id: root.id }), + isHottest + ? tagService.findHottestArticleIds({ + id: root.id, + skip, + take, + }) + : tagService.findArticleIds({ + id: root.id, + // excludeSpam: true, + skip, + take, + }), ]) return connectionFromPromisedArray( diff --git a/src/queries/article/tag/articlesExcludeSpam.ts b/src/queries/article/tag/articlesExcludeSpam.ts index 0856f7fe4..6186db2be 100644 --- a/src/queries/article/tag/articlesExcludeSpam.ts +++ b/src/queries/article/tag/articlesExcludeSpam.ts @@ -7,23 +7,14 @@ const resolver: GQLTagResolvers['articlesExcludeSpam'] = async ( { input }, { dataSources: { tagService, atomService } } ) => { - const { selected, sortBy } = input const { take, skip } = fromConnectionArgs(input) - const isFromRecommendation = - ((root as any).numArticles || (root as any).numAuthors) > 0 - const [totalCount, articleIds] = await Promise.all([ tagService.countArticles({ id: root.id, - selected, - withSynonyms: isFromRecommendation, }), tagService.findArticleIds({ id: root.id, - selected, - sortBy: sortBy as 'byHottestDesc' | 'byCreatedAtDesc' | undefined, - withSynonyms: isFromRecommendation, excludeSpam: true, skip, take, diff --git a/src/queries/article/tag/cover.ts b/src/queries/article/tag/cover.ts deleted file mode 100644 index fbd56c8b3..000000000 --- a/src/queries/article/tag/cover.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -import _find from 'lodash/find' -import _isNil from 'lodash/isNil' - -const resolver: GQLTagResolvers['cover'] = async ( - { id, cover }, - _, - { dataSources: { systemService, tagService } } -) => { - let coverId = cover - - // fall back to first 10 article cover if tag has no cover - if (!coverId) { - const articleCover = _find( - await tagService.findArticleCovers({ id }), - (item) => !_isNil(item.cover) - ) - coverId = articleCover?.cover ?? null - } - return coverId ? systemService.findAssetUrl(coverId) : null -} - -export default resolver diff --git a/src/queries/article/tag/creator.ts b/src/queries/article/tag/creator.ts deleted file mode 100644 index 8f863a326..000000000 --- a/src/queries/article/tag/creator.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -const resolver: GQLTagResolvers['creator'] = ( - { creator }, - _, - { dataSources: { atomService } } -) => { - if (!creator) { - return null - } - - return atomService.userIdLoader.load(creator) -} - -export default resolver diff --git a/src/queries/article/tag/editors.ts b/src/queries/article/tag/editors.ts deleted file mode 100644 index e6e8a2f1f..000000000 --- a/src/queries/article/tag/editors.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -import { environment } from 'common/environment' - -const resolver: GQLTagResolvers['editors'] = ( - { editors, owner }, - { input }, - { dataSources: { atomService } } -) => { - let ids = editors || [] - - if (input?.excludeAdmin === true) { - ids = ids.filter((editor: string) => editor !== environment.mattyId) - } - - if (input?.excludeOwner === true) { - ids = ids.filter((editor: string) => editor !== owner) - } - - return atomService.userIdLoader.loadMany(ids) -} - -export default resolver diff --git a/src/queries/article/tag/followers.ts b/src/queries/article/tag/followers.ts deleted file mode 100644 index 19b64c976..000000000 --- a/src/queries/article/tag/followers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -import { - connectionFromArray, - connectionFromArrayWithKeys, - cursorToKeys, - fromConnectionArgs, -} from 'common/utils' - -const resolver: GQLTagResolvers['followers'] = async ( - { id }, - { input }, - { dataSources: { tagService, atomService } } -) => { - if (!id) { - return connectionFromArray([], input) - } - - const { take } = fromConnectionArgs(input) - const keys = cursorToKeys(input.after) - const params = { targetId: id, skip: keys.idCursor, take } - const [count, actions] = await Promise.all([ - tagService.countFollowers(id), - tagService.findFollowers(params), - ]) - const cursors = actions.reduce( - (map, action) => ({ ...map, [action.userId]: action.id }), - {} - ) - - const users = await atomService.userIdLoader.loadMany( - actions.map(({ userId }: { userId: string }) => userId) - ) - const data = users.map((user) => ({ ...user, __cursor: cursors[user.id] })) - - return connectionFromArrayWithKeys(data, input, count) -} - -export default resolver diff --git a/src/queries/article/tag/isOfficial.ts b/src/queries/article/tag/isOfficial.ts deleted file mode 100644 index 6e902633f..000000000 --- a/src/queries/article/tag/isOfficial.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -import { environment } from 'common/environment' - -const resolver: GQLTagResolvers['isOfficial'] = async ({ id }) => { - const { mattyChoiceTagId } = environment - return id === mattyChoiceTagId -} - -export default resolver diff --git a/src/queries/article/tag/oss.ts b/src/queries/article/tag/oss.ts index 36cff7ab7..245ca9aa4 100644 --- a/src/queries/article/tag/oss.ts +++ b/src/queries/article/tag/oss.ts @@ -11,21 +11,3 @@ export const score: GQLTagOssResolvers['score'] = ( _, { dataSources: { tagService } } ) => tagService.findScore(id) - -export const selected: GQLTagOssResolvers['selected'] = async ( - { id }, - _, - { - dataSources: { - connections: { knex }, - }, - } -) => { - const result = await knex - .from('matters_choice_tag') - .where({ tagId: id }) - .count() - .first() - - return parseInt(result ? (result.count as string) : '0', 10) > 0 -} diff --git a/src/queries/article/tag/owner.ts b/src/queries/article/tag/owner.ts deleted file mode 100644 index 8f607e578..000000000 --- a/src/queries/article/tag/owner.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -const resolver: GQLTagResolvers['owner'] = ( - { owner }, - _, - { dataSources: { atomService } } -) => (owner ? atomService.userIdLoader.load(owner) : null) - -export default resolver diff --git a/src/queries/article/tag/participants.ts b/src/queries/article/tag/participants.ts deleted file mode 100644 index 2b651568b..000000000 --- a/src/queries/article/tag/participants.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -import { - connectionFromArray, - connectionFromPromisedArray, - fromConnectionArgs, -} from 'common/utils' - -const resolver: GQLTagResolvers['participants'] = async ( - { id, owner }, - { input }, - { dataSources: { tagService, atomService } } -) => { - if (!id) { - return connectionFromArray([], input) - } - - const { take, skip } = fromConnectionArgs(input) - - const exclude = owner ? [owner] : [] - const totalCount = await tagService.countParticipants({ id, exclude }) - const userIds = await tagService.findParticipants({ - id, - skip, - take, - exclude, - }) - - return connectionFromPromisedArray( - atomService.userIdLoader.loadMany(userIds.map(({ authorId }) => authorId)), - input, - totalCount - ) -} - -export default resolver diff --git a/src/queries/article/tag/recommendedAuthors.ts b/src/queries/article/tag/recommendedAuthors.ts new file mode 100644 index 000000000..e1519c4ce --- /dev/null +++ b/src/queries/article/tag/recommendedAuthors.ts @@ -0,0 +1,21 @@ +import type { GQLTagResolvers } from 'definitions' + +import { connectionFromArray } from 'common/utils' + +const resolver: GQLTagResolvers['recommendedAuthors'] = async ( + { id }, + { input }, + { dataSources: { tagService, atomService } } +) => { + // const { take, skip } = fromConnectionArgs(input) + + if (!id) { + return connectionFromArray([], input) + } + + return connectionFromArray([], input) + + // return connectionFromPromisedArray(tags.slice(s, end), input, totalCount) +} + +export default resolver diff --git a/src/queries/article/tag/selected.ts b/src/queries/article/tag/selected.ts deleted file mode 100644 index 00ee67b7c..000000000 --- a/src/queries/article/tag/selected.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { GQLTagResolvers } from 'definitions' - -import { ArticleNotFoundError } from 'common/errors' -import { fromGlobalId } from 'common/utils' - -const resolver: GQLTagResolvers['selected'] = async ( - { id }, - { input }, - { dataSources: { tagService, articleService } } -) => { - let articleId: string | undefined - - if (input.id) { - articleId = fromGlobalId(input.id).id - } else if (input.mediaHash) { - const node = await articleService.findVersionByMediaHash(input.mediaHash) - articleId = node.articleId - } - - if (!articleId) { - throw new ArticleNotFoundError('Cannot find article by a given input') - } - - return tagService.isArticleSelected({ - articleId, - tagId: id, - }) -} - -export default resolver diff --git a/src/queries/draft/article/drafts.ts b/src/queries/draft/article/drafts.ts deleted file mode 100644 index 8b1812bdf..000000000 --- a/src/queries/draft/article/drafts.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GQLArticleResolvers } from 'definitions' - -const resolver: GQLArticleResolvers['drafts'] = async ( - { id: articleId }, - _, - { dataSources: { atomService } } -) => - atomService.findMany({ - table: 'draft', - where: { articleId }, - orderBy: [{ column: 'created_at', order: 'desc' }], - }) - -export default resolver diff --git a/src/queries/draft/article/newestPublishedDraft.ts b/src/queries/draft/article/newestPublishedDraft.ts deleted file mode 100644 index 72b4e35b1..000000000 --- a/src/queries/draft/article/newestPublishedDraft.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { GQLArticleResolvers } from 'definitions' - -import { PUBLISH_STATE } from 'common/enums' - -const resolver: GQLArticleResolvers['newestPublishedDraft'] = async ( - { id: articleId }, - _, - { dataSources: { atomService } } -) => { - const draft = await atomService.findFirst({ - table: 'draft', - where: { articleId, publishState: PUBLISH_STATE.published }, - orderBy: [{ column: 'created_at', order: 'desc' }], - }) - return draft -} - -export default resolver diff --git a/src/queries/draft/article/newestUnpublishedDraft.ts b/src/queries/draft/article/newestUnpublishedDraft.ts deleted file mode 100644 index 4c1e31445..000000000 --- a/src/queries/draft/article/newestUnpublishedDraft.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { GQLArticleResolvers } from 'definitions' - -import { PUBLISH_STATE } from 'common/enums' - -const resolver: GQLArticleResolvers['newestUnpublishedDraft'] = async ( - { id: articleId }, - _, - { dataSources: { atomService } } -) => { - const draft = await atomService.findFirst({ - table: 'draft', - where: { articleId }, - whereIn: [ - 'publish_state', - [PUBLISH_STATE.unpublished, PUBLISH_STATE.pending, PUBLISH_STATE.error], - ], - orderBy: [{ column: 'created_at', order: 'desc' }], - }) - - return draft -} - -export default resolver diff --git a/src/queries/draft/index.ts b/src/queries/draft/index.ts index 72004b364..d79fbe55a 100644 --- a/src/queries/draft/index.ts +++ b/src/queries/draft/index.ts @@ -7,9 +7,6 @@ import { NODE_TYPES } from 'common/enums' import { countWords, toGlobalId } from 'common/utils' import * as draftAccess from './access' -import articleDrafts from './article/drafts' -import articleNewestPublishedDraft from './article/newestPublishedDraft' -import articleNewestUnpublishedDraft from './article/newestUnpublishedDraft' import assets from './assets' import campaigns from './campaigns' import collection from './collection' @@ -18,11 +15,6 @@ import draftCover from './cover' import drafts from './drafts' const schema: GQLResolvers = { - Article: { - drafts: articleDrafts, - newestUnpublishedDraft: articleNewestUnpublishedDraft, - newestPublishedDraft: articleNewestPublishedDraft, - }, User: { drafts, }, diff --git a/src/queries/user/liker/index.ts b/src/queries/user/liker/index.ts index 1d391f408..0c24b27b6 100644 --- a/src/queries/user/liker/index.ts +++ b/src/queries/user/liker/index.ts @@ -1,11 +1,9 @@ import civicLiker from './civicLiker' import likerId from './likerId' -import rateUSD from './rateUSD' import total from './total' export default { likerId, civicLiker, total, - rateUSD, } diff --git a/src/queries/user/liker/rateUSD.ts b/src/queries/user/liker/rateUSD.ts deleted file mode 100644 index 099fe1381..000000000 --- a/src/queries/user/liker/rateUSD.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { GQLLikerResolvers } from 'definitions' - -import { getLogger } from 'common/logger' - -const logger = getLogger('query-rate-usd') - -const resolver: GQLLikerResolvers['rateUSD'] = async ( - _, - __: any, - { dataSources: { userService } } -) => { - try { - const price = await userService.likecoin.rate() - - return price - } catch (e) { - logger.error(e) - return null - } -} - -export default resolver diff --git a/src/queries/user/recommendation/hottest.ts b/src/queries/user/recommendation/hottest.ts index c92f610a2..e7707eff1 100644 --- a/src/queries/user/recommendation/hottest.ts +++ b/src/queries/user/recommendation/hottest.ts @@ -11,8 +11,8 @@ import { import { ForbiddenError } from 'common/errors' import { connectionFromPromisedArray, - fromConnectionArgs, excludeSpam, + fromConnectionArgs, } from 'common/utils' export const hottest: GQLRecommendationResolvers['hottest'] = async ( diff --git a/src/queries/user/recommendation/index.ts b/src/queries/user/recommendation/index.ts index 4b506446a..9d6a7fc43 100644 --- a/src/queries/user/recommendation/index.ts +++ b/src/queries/user/recommendation/index.ts @@ -9,14 +9,12 @@ import { icymi } from './icymi' import { icymiTopic } from './icymiTopic' import { newest, newestExcludeSpam } from './newest' import newestCircles from './newestCircles' -import readTagsArticles from './readTagsArticles' import { selectedTags } from './selectedTags' import { tags } from './tags' const resolvers: GQLRecommendationResolvers = { authors, following, - readTagsArticles, hottest, hottestExcludeSpam, icymi, diff --git a/src/queries/user/recommendation/readTagsArticles.ts b/src/queries/user/recommendation/readTagsArticles.ts deleted file mode 100644 index 96391171a..000000000 --- a/src/queries/user/recommendation/readTagsArticles.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { GQLRecommendationResolvers } from 'definitions' - -import { - connectionFromArray, - connectionFromPromisedArray, - fromConnectionArgs, -} from 'common/utils' - -const resolver: GQLRecommendationResolvers['readTagsArticles'] = async ( - { id: userId }, - { input }, - { dataSources: { atomService } } -) => { - if (!userId) { - return connectionFromArray([], input) - } - - const { take, skip } = fromConnectionArgs(input) - - const [totalCount, tagArticles] = await Promise.all([ - atomService.count({ - table: 'recommended_articles_from_read_tags_materialized', - where: { userId }, - }), - atomService.findMany({ - table: 'recommended_articles_from_read_tags_materialized', - where: { userId }, - take, - skip, - }), - ]) - - return connectionFromPromisedArray( - atomService.articleIdLoader.loadMany(tagArticles.map((d) => d.articleId)), - input, - totalCount - ) -} - -export default resolver diff --git a/src/queries/user/recommendation/tags.ts b/src/queries/user/recommendation/tags.ts index 255d35d26..25aab2e81 100644 --- a/src/queries/user/recommendation/tags.ts +++ b/src/queries/user/recommendation/tags.ts @@ -2,8 +2,6 @@ import type { GQLRecommendationResolvers } from 'definitions' import { chunk } from 'lodash' -import { VIEW } from 'common/enums' -// import { environment } from 'common/environment' import { ForbiddenError } from 'common/errors' import { connectionFromPromisedArray, fromConnectionArgs } from 'common/utils' @@ -29,8 +27,7 @@ export const tags: GQLRecommendationResolvers['tags'] = async ( const curationTags = await tagService.findTopTags({ take: limit * draw, - top: 'r2w', - minAuthors: 5, // show at least 5 authors in the curation + minAuthors: 5, }) const chunks = chunk(curationTags, draw) @@ -44,10 +41,7 @@ export const tags: GQLRecommendationResolvers['tags'] = async ( ) } - const totalCount = await tagService.baseCount( - undefined, // where - VIEW.tags_lasts_view - ) + const totalCount = await tagService.countTopTags() const items = await tagService.findTopTags({ skip, take, diff --git a/src/types/__test__/1/article.test.ts b/src/types/__test__/1/article.test.ts index 0e47d73c1..1d613c6f1 100644 --- a/src/types/__test__/1/article.test.ts +++ b/src/types/__test__/1/article.test.ts @@ -106,28 +106,6 @@ const GET_ARTICLE_TAGS = /* GraphQL */ ` } ` -const GET_ARTICLE_DRAFTS = /* GraphQL */ ` - query ($input: NodeInput!) { - node(input: $input) { - ... on Article { - id - drafts { - id - publishState - } - newestPublishedDraft { - id - publishState - } - newestUnpublishedDraft { - id - publishState - } - } - } - } -` - const TOGGLE_SUBSCRIBE_ARTICLE = /* GraphQL */ ` mutation ($input: ToggleItemInput!) { toggleSubscribeArticle(input: $input) { @@ -247,30 +225,6 @@ describe('query tag on article', () => { }) }) -describe('query drafts on article', () => { - test('query drafts on article', async () => { - const id = toGlobalId({ type: NODE_TYPES.Article, id: 4 }) - const server = await testClient({ connections }) - const { data } = await server.executeOperation({ - query: GET_ARTICLE_DRAFTS, - variables: { input: { id } }, - }) - - // drafts - const drafts = data && data.node && data.node.drafts - expect(drafts[0].publishState).toEqual(PUBLISH_STATE.published) - - // unpublishedDraft - const unpublishedDraft = - data && data.node && data.node.newestUnpublishedDraft - expect(unpublishedDraft).toBeNull() - - // publishedDraft - const publishedDraft = data && data.node && data.node.newestPublishedDraft - expect(publishedDraft.publishState).toEqual(PUBLISH_STATE.published) - }) -}) - describe('publish article', () => { test('user w/o username can not publish', async () => { const draft = { diff --git a/src/types/__test__/1/editArticle.test.ts b/src/types/__test__/1/editArticle.test.ts index 90f59d6c8..9c9272162 100644 --- a/src/types/__test__/1/editArticle.test.ts +++ b/src/types/__test__/1/editArticle.test.ts @@ -128,7 +128,7 @@ const EDIT_ARTICLE = /* GraphQL */ ` id content } - sticky + pinned state license requestForDonation @@ -691,7 +691,7 @@ describe('edit article', () => { globalThis.mockEnums.MAX_ARTICLE_REVISION_COUNT = originalCheckRevisionCount }) - test('toggle article sticky', async () => { + test('toggle article pinned', async () => { const server = await testClient({ isAuth: true, connections, @@ -702,22 +702,22 @@ describe('edit article', () => { variables: { input: { id: articleGlobalId, - sticky: true, + pinned: true, }, }, }) - expect(_get(enableResult, 'data.editArticle.sticky')).toBe(true) + expect(_get(enableResult, 'data.editArticle.pinned')).toBe(true) const disableResult = await server.executeOperation({ query: EDIT_ARTICLE, variables: { input: { id: articleGlobalId, - sticky: false, + pinned: false, }, }, }) - expect(_get(disableResult, 'data.editArticle.sticky')).toBe(false) + expect(_get(disableResult, 'data.editArticle.pinned')).toBe(false) }) test('edit license', async () => { diff --git a/src/types/__test__/1/tag.test.ts b/src/types/__test__/1/tag.test.ts index bca5e08f2..f23399e8e 100644 --- a/src/types/__test__/1/tag.test.ts +++ b/src/types/__test__/1/tag.test.ts @@ -1,26 +1,13 @@ -import type { - GQLPutTagInput, - GQLUpdateTagSettingInput, - Connections, -} from 'definitions' +import type { Connections } from 'definitions' import _difference from 'lodash/difference' import _get from 'lodash/get' -import { - NODE_TYPES, - FEATURE_FLAG, - FEATURE_NAME, - UPDATE_TAG_SETTING_TYPE, -} from 'common/enums' +import { NODE_TYPES } from 'common/enums' import { toGlobalId } from 'common/utils' -import { - setFeature, - testClient, - genConnections, - closeConnections, -} from '../utils' +import { testClient, genConnections, closeConnections } from '../utils' +import { TagService } from 'connectors' declare global { // eslint-disable-next-line no-var @@ -43,7 +30,6 @@ const QUERY_TAG = /* GraphQL */ ` ... on Tag { id content - description recommended(input: {}) { edges { node { @@ -58,37 +44,6 @@ const QUERY_TAG = /* GraphQL */ ` } ` -const PUT_TAG = /* GraphQL */ ` - mutation ($input: PutTagInput!) { - putTag(input: $input) { - id - content - description - editors { - id - } - owner { - id - } - } - } -` - -const UPDATE_TAG_SETTING = /* GraphQL */ ` - mutation ($input: UpdateTagSettingInput!) { - updateTagSetting(input: $input) { - id - content - editors { - id - } - owner { - id - } - } - } -` - const RENAME_TAG = /* GraphQL */ ` mutation ($input: RenameTagInput!) { renameTag(input: $input) { @@ -104,9 +59,6 @@ const MERGE_TAG = /* GraphQL */ ` ... on Tag { id content - owner { - id - } } } } @@ -118,151 +70,14 @@ const DELETE_TAG = /* GraphQL */ ` } ` -const ADD_ARTICLES_TAGS = /* GraphQL */ ` - mutation ($input: AddArticlesTagsInput!) { - addArticlesTags(input: $input) { - id - articles(input: { after: null, first: null, oss: true }) { - edges { - node { - ... on Article { - id - } - } - } - } - } - } -` - -const UPDATE_ARTICLES_TAGS = /* GraphQL */ ` - mutation ($input: UpdateArticlesTagsInput!) { - updateArticlesTags(input: $input) { - id - articles(input: { after: null, first: null, oss: true }) { - edges { - node { - ... on Article { - id - } - } - } - } - } - } -` - -const DELETE_ARTICLES_TAGS = /* GraphQL */ ` - mutation ($input: DeleteArticlesTagsInput!) { - deleteArticlesTags(input: $input) { - id - articles(input: { after: null, first: null, oss: true }) { - edges { - node { - ... on Article { - id - } - } - } - } - } - } -` - -interface BaseInput { - isAdmin?: boolean - isAuth?: boolean - isMatty?: boolean -} - -type PutTagInput = { tag: GQLPutTagInput } & BaseInput - -export const putTag = async ({ - isAdmin = true, - isAuth = true, - isMatty = true, - tag, -}: PutTagInput) => { - const server = await testClient({ isAdmin, isAuth, isMatty, connections }) - const result = await server.executeOperation({ - query: PUT_TAG, - variables: { input: tag }, - }) - const data = result?.data?.putTag - return data -} - -type UpdateTagSettingInput = GQLUpdateTagSettingInput & BaseInput - -export const updateTagSetting = async ({ - isAdmin = false, - isAuth = false, - isMatty = false, - id, - type, - editors, -}: UpdateTagSettingInput) => { - const server = await testClient({ isAdmin, isAuth, isMatty, connections }) - const result = await server.executeOperation({ - query: UPDATE_TAG_SETTING, - variables: { input: { id, type, editors } }, - }) - - if (!result.data) { - return result - } - const data = result?.data?.updateTagSetting - return data -} - -describe('put tag', () => { - test('create, query and update tag', async () => { - const content = 'Test tag #1' - const expected = 'Test tag1' - const description = 'This is a tag description' - - // create - const createResult = await putTag({ tag: { content, description } }) - const createTagId = createResult?.id - expect(createTagId).toBeDefined() - - // query - const server = await testClient({ - isAuth: true, - isAdmin: true, - isMatty: true, - connections, - }) - const { data, errors } = await server.executeOperation({ - query: QUERY_TAG, - variables: { input: { id: createTagId } }, - }) - console.log(errors) - expect(data.node.content).toBe(expected) - expect(data.node.description).toBe(description) - - // update - const updateContent = 'Update tag #1' - const updateExpected = 'Update tag1' - const updateDescription = 'Update description' - const updateResult = await putTag({ - tag: { - id: createTagId, - content: updateContent, - description: updateDescription, - }, - }) - expect(updateResult?.content).toBe(updateExpected) - expect(updateResult?.description).toBe(updateDescription) - }) -}) - describe('manage tag', () => { test('rename and delete tag', async () => { - // create - const createResult = await putTag({ tag: { content: 'Test tag #1' } }) - const createTagId = createResult?.id - expect(createTagId).toBeDefined() + const tagService = new TagService(connections) + const tag = await tagService.create({ + content: 'Test tag #1', + creator: '0', + }) + const createTagId = toGlobalId({ type: NODE_TYPES.Tag, id: tag?.id }) const server = await testClient({ isAuth: true, @@ -270,6 +85,7 @@ describe('manage tag', () => { isMatty: true, connections, }) + // rename const renameContent = 'Rename tag' const renameResult = await server.executeOperation({ @@ -286,9 +102,6 @@ describe('manage tag', () => { }) const mergeTagId = mergeResult?.data?.mergeTags?.id expect(mergeResult?.data?.mergeTags?.content).toBe(mergeContent) - expect(mergeResult?.data?.mergeTags?.owner?.id).toBe( - toGlobalId({ type: NODE_TYPES.User, id: 6 }) - ) // delete const deleteResult = await server.executeOperation({ @@ -299,383 +112,6 @@ describe('manage tag', () => { }) }) -describe('manage article tag', () => { - test('users w/o username can not add tags', async () => { - const server = await testClient({ noUserName: true, connections }) - const { errors } = await server.executeOperation({ - query: PUT_TAG, - variables: { input: { content: 'faketag' } }, - }) - expect(errors?.[0].extensions.code).toBe('FORBIDDEN') - }) - test('add and delete article tag', async () => { - // create - const createResult = await putTag({ tag: { content: 'Test tag #1' } }) - const createTagId = createResult?.id - expect(createTagId).toBeDefined() - - const server = await testClient({ - isAuth: true, - isAdmin: true, - isMatty: true, - connections, - }) - - const articleIds = [ - toGlobalId({ type: NODE_TYPES.Article, id: 1 }), - toGlobalId({ type: NODE_TYPES.Article, id: 2 }), - ] - - // add - const addResult = await server.executeOperation({ - query: ADD_ARTICLES_TAGS, - variables: { - input: { - id: createTagId, - articles: articleIds, - }, - }, - }) - expect(addResult?.data?.addArticlesTags?.articles?.edges.length).toBe(2) - - // update - const updateResult = await server.executeOperation({ - query: UPDATE_ARTICLES_TAGS, - variables: { - input: { - id: createTagId, - articles: articleIds, - isSelected: true, - }, - }, - }) - expect(updateResult?.data?.updateArticlesTags?.articles?.edges.length).toBe( - 2 - ) - - // remove - const deleteResult = await server.executeOperation({ - query: DELETE_ARTICLES_TAGS, - variables: { - input: { - id: createTagId, - articles: articleIds, - }, - }, - }) - expect(deleteResult?.data?.deleteArticlesTags?.articles?.edges.length).toBe( - 0 - ) - }) -}) - -describe('manage settings of a tag', () => { - const errorPath = 'errors.0.extensions.code' - const editorFilter = (editor: any) => editor?.id - - test('adopt and leave tag', async () => { - const authedId = toGlobalId({ type: NODE_TYPES.User, id: 1 }) - const mattyId = toGlobalId({ type: NODE_TYPES.User, id: 6 }) - - // matty enable user can adopt tag - await setFeature( - { - isAdmin: true, - isMatty: true, - input: { - name: FEATURE_NAME.tag_adoption, - flag: FEATURE_FLAG.on, - }, - }, - connections - ) - - // matty create tag - const tag = await putTag({ tag: { content: 'Tag adoption #1' } }) - const editors = (tag?.editors || []).map(editorFilter) - expect(editors.includes(mattyId)).toBeTruthy() - expect(tag?.owner?.id).toBe(mattyId) - - // authed user try adopt matty's tag - const adoptMattyTagData = await updateTagSetting({ - isAuth: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.adopt, - }) - expect(_get(adoptMattyTagData, errorPath)).toBe('FORBIDDEN') - - // authed user try to leave matty's tag - const leaveMattyTagData = await updateTagSetting({ - isAuth: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.leave, - }) - expect(_get(leaveMattyTagData, errorPath)).toBe('FORBIDDEN') - - // matty leave tag - const mattyLeaveTagData = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.leave, - }) - const mattyLeaveTagDataEditors = (mattyLeaveTagData?.editors || []).map( - editorFilter - ) - expect(mattyLeaveTagDataEditors.includes(mattyId)).toBeTruthy() - expect(mattyLeaveTagData?.owner).toBe(null) - - // authed user adopt tag - const adoptData = await updateTagSetting({ - isAuth: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.adopt, - }) - const adoptDataEditors = (adoptData?.editors || []).map(editorFilter) - expect(adoptDataEditors.includes(authedId)).toBeTruthy() - expect(adoptData?.owner?.id).toBe(authedId) - - // authed user leave tag - const leaveData = await updateTagSetting({ - isAuth: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.leave, - }) - const leaveDataEditors = (leaveData?.editors || []).map(editorFilter) - expect(leaveDataEditors.includes(authedId)).toBeFalsy() - expect(leaveDataEditors.includes(mattyId)).toBeTruthy() - expect(leaveData?.owner).toBe(null) - }) - - test('add and remove editor to a tag', async () => { - const user1Id = toGlobalId({ type: NODE_TYPES.User, id: 1 }) - const user2Id = toGlobalId({ type: NODE_TYPES.User, id: 2 }) - const user3Id = toGlobalId({ type: NODE_TYPES.User, id: 3 }) - const user4Id = toGlobalId({ type: NODE_TYPES.User, id: 4 }) - const user7Id = toGlobalId({ type: NODE_TYPES.User, id: 7 }) - const mattyId = toGlobalId({ type: NODE_TYPES.User, id: 6 }) - const user9Id = toGlobalId({ type: NODE_TYPES.User, id: 9 }) - - // matty create tag - const tag = await putTag({ tag: { content: 'Tag editor #1' } }) - const editors = (tag?.editors || []).map(editorFilter) - expect(editors.includes(mattyId)).toBeTruthy() - expect(tag?.owner?.id).toBe(mattyId) - - // other try add editor - const otherAddEditorData = await updateTagSetting({ - isAuth: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.add_editor, - editors: [user2Id], - }) - expect(_get(otherAddEditorData, errorPath)).toBe('FORBIDDEN') - - // other try remove editor - const otherRemoveEditorData = await updateTagSetting({ - isAuth: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.remove_editor, - editors: [user2Id], - }) - expect(_get(otherRemoveEditorData, errorPath)).toBe('FORBIDDEN') - - // owner add self into edtor - const addSelfData = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.add_editor, - editors: [mattyId], - }) - const addSelfDataEditors = (addSelfData?.editors || []).map(editorFilter) - expect(addSelfDataEditors.includes(mattyId)).toBeTruthy() - expect(addSelfData?.owner?.id).toBe(mattyId) - - // owner remove self from editor - const rmSelfData = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.remove_editor, - editors: [mattyId], - }) - const rmSelfDataEditors = (rmSelfData?.editors || []).map(editorFilter) - expect(rmSelfDataEditors.includes(mattyId)).toBeTruthy() - expect(rmSelfData?.owner?.id).toBe(mattyId) - - // add other users - const addData1 = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.add_editor, - editors: [user1Id], - }) - const addData1Editors = (addData1?.editors || []).map(editorFilter) - expect(_difference(addData1Editors, [mattyId, user1Id]).length).toBe(0) - expect(addData1?.editors.length).toBe(2) - - const addData2 = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.add_editor, - editors: [user2Id, user3Id, user4Id], - }) - const addData2Editors = (addData2?.editors || []).map(editorFilter) - expect( - _difference(addData2Editors, [ - mattyId, - user1Id, - user2Id, - user3Id, - user4Id, - ]).length - ).toBe(0) - expect(addData2?.editors.length).toBe(5) - - const addData3 = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.add_editor, - editors: [user7Id, user9Id], - }) - expect(_get(addData3, errorPath)).toBe('TAG_EDITORS_REACH_LIMIT') - - // remove other users - const rmData1 = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.remove_editor, - editors: [user4Id], - }) - const rmData1Editors = (rmData1?.editors || []).map(editorFilter) - expect(rmData1Editors.includes(mattyId)).toBeTruthy() - expect(rmData1Editors.includes(user1Id)).toBeTruthy() - expect(rmData1Editors.includes(user2Id)).toBeTruthy() - expect(rmData1Editors.includes(user3Id)).toBeTruthy() - expect(rmData1Editors.includes(user4Id)).toBeFalsy() - expect(rmData1?.editors.length).toBe(4) - - const rmData2 = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.remove_editor, - editors: [user2Id, user3Id], - }) - const rmData2Editors = (rmData2?.editors || []).map(editorFilter) - expect(rmData2Editors.includes(mattyId)).toBeTruthy() - expect(rmData2Editors.includes(user1Id)).toBeTruthy() - expect(rmData2Editors.includes(user2Id)).toBeFalsy() - expect(rmData2Editors.includes(user3Id)).toBeFalsy() - expect(rmData2?.editors.length).toBe(2) - - const rmData3 = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.remove_editor, - editors: [user1Id], - }) - const rmData3Editors = (rmData3?.editors || []).map(editorFilter) - expect(rmData3Editors.includes(mattyId)).toBeTruthy() - expect(rmData3Editors.includes(user1Id)).toBeFalsy() - expect(rmData3?.editors.length).toBe(1) - - // authed user create tag - const authedUserTag = await putTag({ - isAdmin: false, - isMatty: false, - tag: { content: 'Tag editor #2' }, - }) - const authedUserTagEditors = (authedUserTag?.editors || []).map( - editorFilter - ) - expect(_difference(authedUserTagEditors, [mattyId, user1Id]).length).toBe(0) - expect(authedUserTag?.owner?.id).toBe(user1Id) - expect(authedUserTag?.editors.length).toBe(2) - - const authedUserAddData1 = await updateTagSetting({ - isAuth: true, - id: authedUserTag.id, - type: UPDATE_TAG_SETTING_TYPE.remove_editor, - editors: [mattyId], - }) - const authedUserAddData1Editors = (authedUserAddData1?.editors || []).map( - editorFilter - ) - expect( - _difference(authedUserAddData1Editors, [mattyId, user1Id]).length - ).toBe(0) - expect(authedUserAddData1?.editors.length).toBe(2) - - const authedUserAddData2 = await updateTagSetting({ - isAuth: true, - id: authedUserTag.id, - type: UPDATE_TAG_SETTING_TYPE.add_editor, - editors: [user1Id, user2Id], - }) - const authedUserAddData2Editors = (authedUserAddData2?.editors || []).map( - editorFilter - ) - expect( - _difference(authedUserAddData2Editors, [mattyId, user1Id, user2Id]).length - ).toBe(0) - expect(authedUserAddData2?.editors.length).toBe(3) - - const authedUserRmData1 = await updateTagSetting({ - isAuth: true, - id: authedUserTag.id, - type: UPDATE_TAG_SETTING_TYPE.remove_editor, - editors: [mattyId], - }) - const authedUserRmData1Editors = (authedUserRmData1?.editors || []).map( - editorFilter - ) - expect(authedUserRmData1Editors.includes(mattyId)).toBeTruthy() - expect(authedUserRmData1?.editors.length).toBe(3) - }) - - test('leave editor from a tag', async () => { - const user1Id = toGlobalId({ type: NODE_TYPES.User, id: 1 }) - // const user2Id = toGlobalId({ type: NODE_TYPES.User, id: 2 }) - const mattyId = toGlobalId({ type: NODE_TYPES.User, id: 6 }) - - // matty create tag - const tag = await putTag({ tag: { content: 'Tag editor #3' } }) - const editors = (tag?.editors || []).map(editorFilter) - expect(editors.includes(mattyId)).toBeTruthy() - expect(tag?.owner?.id).toBe(mattyId) - - // add editor - const addData = await updateTagSetting({ - isAuth: true, - isMatty: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.add_editor, - editors: [user1Id], - }) - const addDataEditors = (addData?.editors || []).map(editorFilter) - expect(_difference(addDataEditors, [mattyId, user1Id]).length).toBe(0) - expect(addData?.editors.length).toBe(2) - - // user leave from editors - const leaveData = await updateTagSetting({ - isAuth: true, - id: tag.id, - type: UPDATE_TAG_SETTING_TYPE.leave_editor, - editors: [user1Id], - }) - const leaveDataEditors = (leaveData?.editors || []).map(editorFilter) - expect(_difference(leaveDataEditors, [mattyId]).length).toBe(0) - expect(leaveData?.editors.length).toBe(1) - }) -}) - describe('query tag', () => { test('tag recommended', async () => { const server = await testClient({ connections }) diff --git a/src/types/__test__/2/system.test.ts b/src/types/__test__/2/system.test.ts index 16a244aeb..42c44a562 100644 --- a/src/types/__test__/2/system.test.ts +++ b/src/types/__test__/2/system.test.ts @@ -33,9 +33,6 @@ const userDescription = `test-${Math.floor(Math.random() * 100)}` const user = { email: `test-${Math.floor(Math.random() * 100)}@matters.news`, - displayName: 'testUser', - password: '12345678', - codeId: '123', } let connections: Connections @@ -44,7 +41,7 @@ beforeAll(async () => { connections = await genConnections() const { id } = await putDraft({ draft }, connections) await publishArticle({ id }, connections) - await registerUser(user, connections) + await registerUser(user.email, connections) await updateUserDescription( { email: user.email, diff --git a/src/types/__test__/2/user.test.ts b/src/types/__test__/2/user.test.ts index ba089f496..aac172240 100644 --- a/src/types/__test__/2/user.test.ts +++ b/src/types/__test__/2/user.test.ts @@ -29,11 +29,11 @@ import { import { defaultTestUser, getUserContext, - registerUser, testClient, updateUserState, genConnections, closeConnections, + registerUser, } from '../utils' let connections: Connections @@ -54,9 +54,9 @@ afterAll(async () => { await closeConnections(connections) }) -const USER_LOGIN = /* GraphQL */ ` - mutation UserLogin($input: UserLoginInput!) { - userLogin(input: $input) { +const EMAIL_LOGIN = /* GraphQL */ ` + mutation EmailLogin($input: EmailLoginInput!) { + emailLogin(input: $input) { auth token } @@ -410,17 +410,6 @@ const RESET_USER_LIKER_ID = /* GraphQL */ ` } ` -const RESET_USER_WALLET = /* GraphQL */ ` - mutation ResetWallet($input: ResetWalletInput!) { - resetWallet(input: $input) { - id - info { - ethAddress - } - } - } -` - describe('register and login functionarlities', () => { test('register user and retrieve info', async () => { const email = `test-${Math.floor(Math.random() * 100)}@matters.news` @@ -434,12 +423,10 @@ describe('register and login functionarlities', () => { }) const user = { email, - displayName: 'testUser', - password: 'Abcd1234', - codeId: code.uuid, + passwordOrCode: code.code, } - const registerResult = await registerUser(user, connections) - expect(_get(registerResult, 'data.userRegister.token')).toBeTruthy() + const registerResult = await registerUser(user.email, connections) + expect(_get(registerResult, 'data.emailLogin.token')).toBeTruthy() const context = await getUserContext({ email: user.email }, connections) const server = await testClient({ @@ -449,9 +436,7 @@ describe('register and login functionarlities', () => { const newUserResult = await server.executeOperation({ query: GET_VIEWER_INFO, }) - const displayName = _get(newUserResult, 'data.viewer.displayName') const info = newUserResult!.data!.viewer.info - expect(displayName).toBe(user.displayName) expect(info.email).toBe(user.email) const status = newUserResult!.data!.viewer.status @@ -464,8 +449,8 @@ describe('register and login functionarlities', () => { const server = await testClient({ connections }) const result = await server.executeOperation({ - query: USER_LOGIN, - variables: { input: { email, password } }, + query: EMAIL_LOGIN, + variables: { input: { email, passwordOrCode: password } }, }) expect(_get(result, 'errors.0.extensions.code')).toBe( 'USER_PASSWORD_INVALID' @@ -478,10 +463,10 @@ describe('register and login functionarlities', () => { const server = await testClient({ connections }) const result = await server.executeOperation({ - query: USER_LOGIN, - variables: { input: { email, password } }, + query: EMAIL_LOGIN, + variables: { input: { email, passwordOrCode: password } }, }) - expect(_get(result, 'data.userLogin.auth')).toBe(true) + expect(_get(result, 'data.emailLogin.auth')).toBe(true) }) test('retrive user info after login', async () => { @@ -904,19 +889,6 @@ describe('mutations on User object', () => { expect(adminReservedNameDisplayName).toEqual(RESERVED_NAMES[0]) }) - test('updateUserInfoUserName', async () => { - const server = await testClient({ isAuth: true, connections }) - - const userName2 = 'UPPERTest' - const { data } = await server.executeOperation({ - query: UPDATE_USER_INFO, - variables: { input: { userName: userName2 } }, - }) - expect(_get(data, 'updateUserInfo.userName')).toEqual( - userName2.toLowerCase() - ) - }) - test('updateUserInfoDescription', async () => { const description = 'foo bar' const server = await testClient({ @@ -1060,7 +1032,6 @@ describe('user recommendations', () => { test('retrieve tags from tags', async () => { await refreshView(MATERIALIZED_VIEW.curation_tag_materialized, knex) - await refreshView(MATERIALIZED_VIEW.tag_count_materialized, knex) const serverNew = await testClient({ isAuth: true, @@ -1337,51 +1308,6 @@ describe('likecoin', () => { }) }) -describe('crypto wallet', () => { - test('reset wallet', async () => { - const server = await testClient({ - isAuth: true, - isAdmin: true, - connections, - }) - - // check if exists - const { data } = await server.executeOperation({ - query: GET_USER_BY_USERNAME, - variables: { input: { userName: 'test2' } }, - }) - - // reset - const resetResult = await server.executeOperation({ - query: RESET_USER_WALLET, - variables: { input: { id: _get(data, 'user.id') } }, - }) - expect(_get(resetResult, 'data.resetWallet.id')).toBe(_get(data, 'user.id')) - expect(_get(resetResult, 'data.resetWallet.info.ethAddress')).toBeFalsy() - }) - - test('reset wallet forbidden', async () => { - const server = await testClient({ - isAuth: true, - isAdmin: true, - connections, - }) - - // check if exists - const { data } = await server.executeOperation({ - query: GET_USER_BY_USERNAME, - variables: { input: { userName: 'test10' } }, - }) - - // reset - const resetResult = await server.executeOperation({ - query: RESET_USER_WALLET, - variables: { input: { id: _get(data, 'user.id') } }, - }) - expect(_get(resetResult, 'data.resetWallet.id')).toBeFalsy() - }) -}) - describe('update user state', () => { // archive user const id = toGlobalId({ type: NODE_TYPES.User, id: '1' }) diff --git a/src/types/__test__/utils.ts b/src/types/__test__/utils.ts index dfaa85823..4ce6ee4cb 100644 --- a/src/types/__test__/utils.ts +++ b/src/types/__test__/utils.ts @@ -2,7 +2,6 @@ import type { GQLPublishArticleInput, GQLPutDraftInput, GQLSetFeatureInput, - GQLUserRegisterInput, User, Connections, DataSources, @@ -42,6 +41,7 @@ import { import { genConnections, closeConnections } from 'connectors/__test__/utils' import schema from '../../schema' +import { VERIFICATION_CODE_STATUS } from 'common/enums' export { genConnections, closeConnections } @@ -326,23 +326,30 @@ export const putDraft = async ( return putDraftResult } -export const registerUser = async ( - user: GQLUserRegisterInput, - connections: Connections -) => { - const USER_REGISTER = ` - mutation UserRegister($input: UserRegisterInput!) { - userRegister(input: $input) { +export const registerUser = async (email: string, connections: Connections) => { + const EMAIL_REGISTER = ` + mutation EmailRegister($input: EmailLoginInput!) { + emailLogin(input: $input) { auth token } } ` + const userService = new UserService(connections) + const code = await userService.createVerificationCode({ + type: 'register', + email, + }) + await userService.markVerificationCodeAs({ + codeId: code.id, + status: VERIFICATION_CODE_STATUS.verified, + }) + const server = await testClient({ connections }) return server.executeOperation({ - query: USER_REGISTER, - variables: { input: user }, + query: EMAIL_REGISTER, + variables: { input: { email, passwordOrCode: code.code } }, }) } diff --git a/src/types/article.ts b/src/types/article.ts index 8f8eb5859..4df8eb9d4 100644 --- a/src/types/article.ts +++ b/src/types/article.ts @@ -18,8 +18,9 @@ export default /* GraphQL */ ` "Edit an article." editArticle(input: EditArticleInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @purgeCache(type: "${NODE_TYPES.Article}") - "Subscribe or Unsubscribe article" - toggleSubscribeArticle(input: ToggleItemInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Article}") + "Bookmark or unbookmark article" + toggleSubscribeArticle(input: ToggleItemInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Article}") @deprecated(reason: "Use toggleBookmarkArticle instead") + toggleBookmarkArticle(input: ToggleItemInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Article}") "Appreciate an article." appreciateArticle(input: AppreciateArticleInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @purgeCache(type: "${NODE_TYPES.Article}") @rateLimit(limit:5, period:60) @@ -31,24 +32,9 @@ export default /* GraphQL */ ` ############## # Tag # ############## - "Follow or unfollow tag." - toggleFollowTag(input: ToggleItemInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") - - "Create or update tag." - putTag(input: PutTagInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") - - "Update member, permission and othters of a tag." - updateTagSetting(input: UpdateTagSettingInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") - - "Add one tag to articles." - addArticlesTags(input: AddArticlesTagsInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") - - "Update articles' tag." - updateArticlesTags(input: UpdateArticlesTagsInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") - - "Delete one tag from articles" - deleteArticlesTags(input: DeleteArticlesTagsInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") - + "Bookmark or unbookmark tag." + toggleFollowTag(input: ToggleItemInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") @deprecated(reason: "Use toggleBookmarkTag instead") + toggleBookmarkTag(input: ToggleItemInput!): Tag! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Tag}") ############## # OSS # @@ -57,7 +43,6 @@ export default /* GraphQL */ ` updateArticleState(input: UpdateArticleStateInput!): Article! @auth(mode: "${AUTH_MODE.admin}") @purgeCache(type: "${NODE_TYPES.Article}") updateArticleSensitive(input: UpdateArticleSensitiveInput!): Article! @auth(mode: "${AUTH_MODE.admin}") @purgeCache(type: "${NODE_TYPES.Article}") - toggleTagRecommend(input: ToggleRecommendInput!): Tag! @auth(mode: "${AUTH_MODE.admin}") @purgeCache(type: "${NODE_TYPES.Tag}") deleteTags(input: DeleteTagsInput!): Boolean @complexity(value: 10, multipliers: ["input.ids"]) @auth(mode: "${AUTH_MODE.admin}") renameTag(input: RenameTagInput!): Tag! @auth(mode: "${AUTH_MODE.admin}") @purgeCache(type: "${NODE_TYPES.Tag}") mergeTags(input: MergeTagsInput!): Tag! @complexity(value: 10, multipliers: ["input.ids"]) @auth(mode: "${AUTH_MODE.admin}") @purgeCache(type: "${NODE_TYPES.Tag}") @@ -154,9 +139,6 @@ export default /* GraphQL */ ` "Total number of readers of this article." readerCount: Int! @cacheControl(maxAge: ${CACHE_TTL.SHORT}) - "Subscribers of this article." - subscribers(input: ConnectionArgs!): UserConnection! @complexity(multipliers: ["input.first"], value: 1) - "Limit the nuhmber of appreciate per user." appreciateLimit: Int! @@ -169,11 +151,11 @@ export default /* GraphQL */ ` "This value determines if current viewer can SuperLike or not." canSuperLike: Boolean! - "This value determines if current Viewer has subscribed of not." - subscribed: Boolean! + "This value determines if current Viewer has bookmarked of not." + subscribed: Boolean! @deprecated(reason: "Use bookmarked instead") + bookmarked: Boolean! "This value determines if this article is an author selected article or not." - sticky: Boolean! @deprecated(reason: "Use pinned instead") pinned: Boolean! "Translation of article title and content." @@ -191,15 +173,6 @@ export default /* GraphQL */ ` "Cumulative reading time in seconds" readTime: Float! - "Drafts linked to this article." - drafts: [Draft!] @logCache(type: "${NODE_TYPES.Draft}") @deprecated(reason: "Use Article.newestUnpublishedDraft or Article.newestPublishedDraft instead") - - "Newest unpublished draft linked to this article." - newestUnpublishedDraft: Draft @logCache(type: "${NODE_TYPES.Draft}") - - "Newest published draft linked to this article." - newestPublishedDraft: Draft! @logCache(type: "${NODE_TYPES.Draft}") - "Revision Count" revisionCount: Int! @@ -293,48 +266,21 @@ export default /* GraphQL */ ` articlesExcludeSpam(input: TagArticlesInput!): ArticleConnection! @complexity(multipliers: ["input.first"], value: 1) - "This value determines if this article is selected by this tag or not." - selected(input: TagSelectedInput!): Boolean! - "Time of this tag was created." createdAt: DateTime! - "Tag's cover link." - cover: String - - "Description of this tag." - description: String - - "Editors of this tag." - editors(input: TagEditorsInput): [User!] @logCache(type: "${NODE_TYPES.User}") - - "Creator of this tag." - creator: User @logCache(type: "${NODE_TYPES.User}") - - "Owner of this tag." - owner: User - "This value determines if current viewer is following or not." isFollower: Boolean - "Followers of this tag." - followers(input: ConnectionArgs!): UserConnection! @complexity(multipliers: ["input.first"], value: 1) - - "Participants of this tag." - participants(input: ConnectionArgs!): UserConnection! @complexity(multipliers: ["input.first"], value: 1) - "Tags recommended based on relations to current tag." recommended(input: ConnectionArgs!): TagConnection! @complexity(multipliers: ["input.first"], value: 1) - "This value determines if it is official." - isOfficial: Boolean + "Authors recommended based on relations to current tag." + recommendedAuthors(input: ConnectionArgs!): UserConnection! @complexity(multipliers: ["input.first"], value: 1) "Counts of this tag." numArticles: Int! @objectCache(maxAge: ${CACHE_TTL.MEDIUM}) ## cache for 1 hour numAuthors: Int! @objectCache(maxAge: ${CACHE_TTL.MEDIUM}) ## cache for 1 hour - ## numArticlesR3m: Int - ## numAuthorsR3m: Int - ############## # OSS # @@ -386,7 +332,6 @@ export default /* GraphQL */ ` type TagOSS @cacheControl(maxAge: ${CACHE_TTL.INSTANT}) { boost: Float! score: Float! - selected: Boolean! } type ArticleConnection implements Connection { @@ -442,8 +387,6 @@ export default /* GraphQL */ ` input EditArticleInput { id: ID! state: ArticleState - "deprecated, use pinned instead" - sticky: Boolean pinned: Boolean title: String summary: String @@ -489,6 +432,7 @@ export default /* GraphQL */ ` id: ID! } + input ToggleRecommendInput { id: ID! enabled: Boolean! @@ -519,36 +463,6 @@ export default /* GraphQL */ ` content: String! } - input PutTagInput { - id: ID - content: String - cover: ID - description: String - } - - input UpdateTagSettingInput { - id: ID! - type: UpdateTagSettingType! - editors: [ID!] - } - - input AddArticlesTagsInput { - id: ID! - articles: [ID!] - selected: Boolean - } - - input UpdateArticlesTagsInput { - id: ID! - articles: [ID!] - isSelected: Boolean! - } - - input DeleteArticlesTagsInput { - id: ID! - articles: [ID!] - } - enum TagArticlesSortBy { byHottestDesc byCreatedAtDesc @@ -558,20 +472,9 @@ export default /* GraphQL */ ` after: String first: Int @constraint(min: 0) oss: Boolean - selected: Boolean sortBy: TagArticlesSortBy = byCreatedAtDesc } - input TagSelectedInput { - id: ID - mediaHash: String - } - - input TagEditorsInput { - excludeAdmin: Boolean - excludeOwner: Boolean - } - input TransactionsReceivedByArgs { after: String first: Int @constraint(min: 0) @@ -620,12 +523,4 @@ export default /* GraphQL */ ` newest search } - - enum UpdateTagSettingType { - adopt - leave - add_editor - remove_editor - leave_editor - } ` diff --git a/src/types/user.ts b/src/types/user.ts index 8a3f5d5d5..68aff3452 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -17,9 +17,6 @@ export default /* GraphQL */ ` "Reset user or payment password." resetPassword(input: ResetPasswordInput!): Boolean - "Change user email." - changeEmail(input: ChangeEmailInput!): User! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @purgeCache(type: "${NODE_TYPES.User}") @deprecated(reason: "use 'setEmail' instead") - "Set user email." setEmail(input: SetEmailInput!): User! @auth(mode: "oauth") @purgeCache(type: "${NODE_TYPES.User}") @@ -29,12 +26,7 @@ export default /* GraphQL */ ` "Set user currency preference." setCurrency(input: SetCurrencyInput!): User! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.User}") - "Register user, can only be used on matters.{town,news} website." - userRegister(input: UserRegisterInput!): AuthResult! @deprecated(reason: "use 'emailLogin' instead") @rateLimit(limit:10, period:86400) - "Login user." - userLogin(input: UserLoginInput!): AuthResult! @deprecated(reason: "use 'emailLogin' instead") - emailLogin(input: EmailLoginInput!): AuthResult! "Get signing message." @@ -58,15 +50,9 @@ export default /* GraphQL */ ` "Remove a social login from current user." removeSocialLogin(input: RemoveSocialLoginInput!): User! @auth(mode: "oauth") @purgeCache(type: "${NODE_TYPES.User}") - "Reset crypto wallet." - resetWallet(input: ResetWalletInput!): User! @auth(mode: "${AUTH_MODE.admin}") @purgeCache(type: "${NODE_TYPES.User}") @deprecated(reason: "use 'removeWalletLogin' instead") - "Logout user." userLogout: Boolean! - "Generate or claim a Liker ID through LikeCoin" - generateLikerId: User! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @purgeCache(type: "${NODE_TYPES.User}") @deprecated(reason: "No longer in use") - "Reset Liker ID" resetLikerId(input: ResetLikerIdInput!): User! @auth(mode: "${AUTH_MODE.admin}") @purgeCache(type: "${NODE_TYPES.User}") @@ -223,9 +209,6 @@ export default /* GraphQL */ ` "Activities based on user's following, sort by creation time." following(input: RecommendationFollowingInput!): FollowingActivityConnection! @complexity(multipliers: ["input.first"], value: 1) - "Articles recommended based on recently read article tags." - readTagsArticles(input: ConnectionArgs!): ArticleConnection! @complexity(multipliers: ["input.first"], value: 1) @deprecated(reason: "Merged into following") - "Global articles sort by publish time." newest(input: ConnectionArgs!): ArticleConnection! @complexity(multipliers: ["input.first"], value: 1) @cacheControl(maxAge: ${CACHE_TTL.PUBLIC_FEED_ARTICLE}) @@ -432,9 +415,6 @@ export default /* GraphQL */ ` "Total LIKE left in wallet." total: Float! @auth(mode: "${AUTH_MODE.oauth}") - - "Rate of LikeCoin/USD" - rateUSD: Float @objectCache(maxAge: ${CACHE_TTL.LONG}) @deprecated(reason: "No longer in use") } type UserOSS @cacheControl(maxAge: ${CACHE_TTL.INSTANT}) { @@ -738,13 +718,6 @@ export default /* GraphQL */ ` type: ResetPasswordType } - input ChangeEmailInput { - oldEmail: String! @constraint(format: "email") - oldEmailCodeId: ID! - newEmail: String! @constraint(format: "email") - newEmailCodeId: ID! - } - input VerifyEmailInput { email: String! code: String! @@ -754,21 +727,6 @@ export default /* GraphQL */ ` currency: QuoteCurrency } - input UserRegisterInput { - email: String! @constraint(format: "email") - userName: String - displayName: String! - password: String! - description: String - codeId: ID! - referralCode: String - } - - input UserLoginInput { - email: String! @constraint(format: "email") - password: String! - } - input GenerateSigningMessageInput { address: String! purpose: SigningMessagePurpose @@ -786,12 +744,6 @@ export default /* GraphQL */ ` "nonce from generateSigningMessage" nonce: String! - "required for wallet register" - email: String @constraint(format: "email") @deprecated(reason: "No longer in use") - - "email verification code, required for wallet register" - codeId: ID @deprecated(reason: "No longer in use") - "used in register" language: UserLanguage @@ -802,10 +754,6 @@ export default /* GraphQL */ ` id: ID! } - input ResetWalletInput { - id: ID! - } - input UpdateNotificationSettingInput { type: NotificationSettingType! enabled: Boolean! @@ -813,7 +761,6 @@ export default /* GraphQL */ ` input UpdateUserInfoInput { displayName: String - userName: String @deprecated(reason: "use 'setUserName' instead") avatar: ID description: String language: UserLanguage @@ -911,9 +858,6 @@ export default /* GraphQL */ ` register email_verify email_otp - email_reset @deprecated(reason: "No longer in use") - email_reset_confirm @deprecated(reason: "No longer in use") - password_reset @deprecated(reason: "No longer in use") payment_password_reset }