diff --git a/db/migrations/20240130120100_alter_article_and_draft_drop_fields.js b/db/migrations/20240130120100_alter_article_and_draft_drop_fields.js new file mode 100644 index 000000000..229c97f6b --- /dev/null +++ b/db/migrations/20240130120100_alter_article_and_draft_drop_fields.js @@ -0,0 +1,35 @@ +exports.up = async (knex) => { + await knex.schema.table('article', (t) => { + t.dropColumn('uuid') + t.dropColumn('title') + t.dropColumn('slug') + t.dropColumn('cover') + t.dropColumn('upstream_id') + t.dropColumn('live') + t.dropColumn('public') + t.dropColumn('content') + t.dropColumn('summary') + t.dropColumn('language') + t.dropColumn('data_hash') + t.dropColumn('media_hash') + t.dropColumn('iscn_id') + t.dropColumn('draft_id') + }) + + await knex.schema.table('draft', (t) => { + t.dropColumn('uuid') + t.dropColumn('summary_customized') + t.dropColumn('word_count') + t.dropColumn('language') + t.dropColumn('data_hash') + t.dropColumn('media_hash') + t.dropColumn('prev_draft_id') + t.dropColumn('iscn_id') + t.dropColumn('pin_state') + t.dropColumn('sensitive_by_admin') + }) +} + +exports.down = async () => { + // do nothing +} diff --git a/db/migrations/20240529145909_migrate_article_empty_title.js b/db/migrations/20240529145909_migrate_article_empty_title.js new file mode 100644 index 000000000..ce4acc11b --- /dev/null +++ b/db/migrations/20240529145909_migrate_article_empty_title.js @@ -0,0 +1,16 @@ +exports.up = async (knex) => { + // affect prod articles with id in (243998,262321,253780,262709,262585,262308,264186,263124,263668,265021,265465,268994,266555,267068,270498,265965,268372,267582,268126,268449,269981,269510,269294,270127,271473,270296,270958,274629,272220,273592,271769,272654,273154,273360,274074,274992,275036,275411,275909,276389,276046,276463,277421,276917,277425,277473,278627,277712,277905,278236,278033,279982,279524,279071,280941,280409,280942,281768,281276,280908,283634,282596,283018,282173,282228,282374,283452,283455,284594,284154,283836,283922,285028,389948,285432,311294,349943,359939,439395,439369,439370,420681,434821,439407,439410,439408,439415,439413,439412,439371,439414,439416,439368,439372,439375,439378,439380,439382,439383,439385,439386,439387,439388,439389,439390,439391,439392,439393,465682,439396,439397,439398,439400,439401,439405,453057,63347,469642,469640,57283,2554,29952,15420,154190,39433,39482,100932,40361,115292,122903,242266,184759,184812) + await knex.raw(` + UPDATE article_version SET title='未命名' + WHERE id IN ( + SELECT avn.id FROM article_version_newest avn + JOIN article a + ON avn.article_id = a.id + WHERE avn.title ~ '^[[:space:]\\u00a0\\u180e\\u2007\\u200b-\\u200f\\u202f\\u2060\\ufeff\\u3000]*$' + AND state='active' + );`) +} + +exports.down = async () => { + // do nothing +} diff --git a/db/migrations/20240612155317_update_user_reader_materialized_view.js b/db/migrations/20240612155317_update_user_reader_materialized_view.js new file mode 100644 index 000000000..8bddfdfe7 --- /dev/null +++ b/db/migrations/20240612155317_update_user_reader_materialized_view.js @@ -0,0 +1,110 @@ +const view = `user_reader_view` +const materialized = `user_reader_materialized` + +exports.up = async (knex) => { + await knex.raw(/*sql*/ ` + drop view ${view} cascade; + + create view ${view} as + SELECT "user".id, + "user".uuid, + "user".user_name, + "user".display_name, + "user".description, + "user".avatar, + "user".email, + "user".email_verified, + "user".mobile, + "user".password_hash, + "user".read_speed, + "user".base_gravity, + "user".curr_gravity, + "user".language, + "user".role, + "user".state, + "user".created_at, + "user".updated_at, + a.recent_donation, + r.recent_readtime, + (COALESCE(a.recent_donation, 0::bigint)+1)*COALESCE(r.recent_readtime, 0::bigint)*COALESCE(b.boost, 1::real) AS author_score + FROM "user" + LEFT JOIN ( SELECT ts.recipient_id, count(1) AS recent_donation + FROM transaction ts + JOIN article ON ts.target_id = article.id + WHERE ts.created_at >= now() - interval '1 week' + AND ts.state = 'succeeded' + AND ts.purpose = 'donation' + AND ts.currency = 'HKD' + AND ts.target_type=4 + AND article.state='active' + GROUP BY ts.recipient_id) a ON a.recipient_id = "user".id + LEFT JOIN (SELECT a2.author_id, sum(read_time) AS recent_readtime + FROM article_read_count a1 + JOIN article a2 ON a1.article_id = a2.id + WHERE a1.created_at >= now() - interval '1 week' + AND a1.user_id is not null + AND a2.state = 'active' + GROUP BY a2.author_id) r ON r.author_id = "user".id + LEFT JOIN ( SELECT boost, + user_id + FROM user_boost) b ON "user".id = b.user_id + where "user".state not in ('banned', 'frozen') and "user".id != 81 + order by author_score desc; + + create materialized view ${materialized} as + select * from ${view}; + + CREATE UNIQUE INDEX ${materialized}_id on public.${materialized} (id); + CREATE INDEX ${materialized}_author_score on public.${materialized} (author_score); + `) +} + +exports.down = async (knex) => { + await knex.raw(/*sql*/ ` + drop view ${view} cascade; + + create view ${view} as + SELECT "user".id, + "user".uuid, + "user".user_name, + "user".display_name, + "user".description, + "user".avatar, + "user".email, + "user".email_verified, + "user".mobile, + "user".password_hash, + "user".read_speed, + "user".base_gravity, + "user".curr_gravity, + "user".language, + "user".role, + "user".state, + "user".created_at, + "user".updated_at, + a.recent_donation, + r.recent_readtime, + (COALESCE(a.recent_donation, 0::bigint)+1)*COALESCE(r.recent_readtime, 0::bigint)*COALESCE(b.boost, 1::real) AS author_score + FROM "user" + LEFT JOIN ( SELECT ts.recipient_id, count(1) AS recent_donation + FROM transaction ts + WHERE ts.created_at >= now() - interval '1 week' AND ts.state = 'succeeded' AND ts.purpose = 'donation' AND ts.currency = 'HKD' + GROUP BY ts.recipient_id) a ON a.recipient_id = "user".id + LEFT JOIN (SELECT a2.author_id, sum(read_time) AS recent_readtime + FROM article_read_count a1 + JOIN article a2 ON a1.article_id = a2.id + WHERE a1.created_at >= now() - interval '1 week' + AND a1.user_id is not null + GROUP BY a2.author_id) r ON r.author_id = "user".id + LEFT JOIN ( SELECT boost, + user_id + FROM user_boost) b ON "user".id = b.user_id + where "user".state not in ('banned', 'frozen') and "user".id != 81 + order by author_score desc; + + create materialized view ${materialized} as + select * from ${view}; + + CREATE UNIQUE INDEX ${materialized}_id on public.${materialized} (id); + `) +} diff --git a/package-lock.json b/package-lock.json index 9b6e54193..bc6e209b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@keyv/redis": "^2.6.1", "@matters/apollo-response-cache": "^2.0.0-alpha.0", "@matters/ipns-site-generator": "^0.1.6", - "@matters/matters-editor": "^0.2.4", + "@matters/matters-editor": "^0.2.5-alpha.0", "@matters/passport-likecoin": "^1.0.0", "@matters/slugify": "^0.7.3", "@sendgrid/helpers": "^7.7.0", @@ -5009,9 +5009,9 @@ } }, "node_modules/@matters/matters-editor": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.4.tgz", - "integrity": "sha512-dmGVnmdgpSekfxEyj1Or5yoNEzky395ZMPFUckyVYILKGvlWFuGnbAWTzEc5vFUtHcFPKqfPZt0r/ocnWyKevg==", + "version": "0.2.5-alpha.0", + "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.5-alpha.0.tgz", + "integrity": "sha512-lPFGJG3pPa5CkxqExgObO+3zz3GY6D6/oFNB8KILhXpwuB7y454VP6n1ez+Wn1rdWAxbS4qLGuMslfKOCFzfnA==", "dependencies": { "@tiptap/core": "2.2.4", "@tiptap/extension-blockquote": "2.2.4", @@ -26879,9 +26879,9 @@ } }, "@matters/matters-editor": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.4.tgz", - "integrity": "sha512-dmGVnmdgpSekfxEyj1Or5yoNEzky395ZMPFUckyVYILKGvlWFuGnbAWTzEc5vFUtHcFPKqfPZt0r/ocnWyKevg==", + "version": "0.2.5-alpha.0", + "resolved": "https://registry.npmjs.org/@matters/matters-editor/-/matters-editor-0.2.5-alpha.0.tgz", + "integrity": "sha512-lPFGJG3pPa5CkxqExgObO+3zz3GY6D6/oFNB8KILhXpwuB7y454VP6n1ez+Wn1rdWAxbS4qLGuMslfKOCFzfnA==", "requires": { "@tiptap/core": "2.2.4", "@tiptap/extension-blockquote": "2.2.4", diff --git a/package.json b/package.json index c058f5ac1..ca7b7ede4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-server", - "version": "5.0.3", + "version": "5.0.4", "description": "Matters Server", "author": "Matters ", "main": "build/index.js", @@ -50,7 +50,7 @@ "@keyv/redis": "^2.6.1", "@matters/apollo-response-cache": "^2.0.0-alpha.0", "@matters/ipns-site-generator": "^0.1.6", - "@matters/matters-editor": "^0.2.4", + "@matters/matters-editor": "^0.2.5-alpha.0", "@matters/passport-likecoin": "^1.0.0", "@matters/slugify": "^0.7.3", "@sendgrid/helpers": "^7.7.0", diff --git a/src/common/enums/index.ts b/src/common/enums/index.ts index 312acf4c1..80311a18e 100644 --- a/src/common/enums/index.ts +++ b/src/common/enums/index.ts @@ -240,7 +240,6 @@ export const MAX_ARTICLES_PER_CONNECTION_LIMIT = 3 export const MAX_ARTICLE_CONTENT_REVISION_LENGTH = 50 export const MAX_ARTICLE_COMMENT_LENGTH = 1200 -export const MAX_COMMENT_EMPTY_PARAGRAPHS = 1 export const MAX_TAGS_PER_ARTICLE_LIMIT = 3 export const TAGS_RECOMMENDED_LIMIT = 100 diff --git a/src/connectors/__test__/articleService.test.ts b/src/connectors/__test__/articleService.test.ts index c9fbba37d..ee4ed9152 100644 --- a/src/connectors/__test__/articleService.test.ts +++ b/src/connectors/__test__/articleService.test.ts @@ -139,28 +139,43 @@ describe('findByAuthor', () => { expect(articles2[0].id).toBe('1') }) test('order by amount of appreciations', async () => { - const draftIds = await articleService.findByAuthor('1', { + const articles = await articleService.findByAuthor('1', { orderBy: 'mostAppreciations', }) - expect(draftIds.length).toBeDefined() + expect(articles.length).toBeDefined() }) test('order by num of comments', async () => { - const draftIds = await articleService.findByAuthor('1', { + const articles = await articleService.findByAuthor('1', { orderBy: 'mostComments', }) - expect(draftIds.length).toBeDefined() + expect(articles.length).toBeDefined() }) test('order by num of donations', async () => { - const draftIds = await articleService.findByAuthor('1', { + const articles = await articleService.findByAuthor('1', { orderBy: 'mostDonations', }) - expect(draftIds.length).toBeDefined() + expect(articles.length).toBeDefined() }) test('filter by state', async () => { - const draftIds = await articleService.findByAuthor('1', { + const articles = await articleService.findByAuthor('1', { state: 'archived', }) - expect(draftIds.length).toBeDefined() + expect(articles.length).toBeDefined() + }) + test('excludeRestricted', async () => { + const articles = await articleService.findByAuthor('1', { + excludeRestricted: true, + }) + expect(articles.length).toBeDefined() + + await atomService.create({ + table: 'article_recommend_setting', + data: { articleId: articles[0].id, inNewest: true, inHottest: false }, + }) + const excluded = await articleService.findByAuthor('1', { + excludeRestricted: true, + }) + expect(excluded).not.toContain(articles[0]) }) }) diff --git a/src/connectors/__test__/commentService.test.ts b/src/connectors/__test__/commentService.test.ts index 141edffc7..2b046860c 100644 --- a/src/connectors/__test__/commentService.test.ts +++ b/src/connectors/__test__/commentService.test.ts @@ -1,6 +1,8 @@ import { v4 as uuidv4 } from 'uuid' import type { Connections } from 'definitions' +import { COMMENT_STATE, COMMENT_TYPE } from 'common/enums' + import { CommentService, AtomService } from 'connectors' import { genConnections, closeConnections } from './utils' @@ -29,6 +31,39 @@ describe('find subcomments by parent comment id', () => { const [comments, count] = await commentService.findByParent({ id: '1' }) expect(comments.length).toBeGreaterThan(0) expect(count).toBeGreaterThan(0) + + // archived/banned comments excluded + const { id: targetTypeId } = await atomService.findFirst({ + table: 'entity_type', + where: { table: 'article' }, + }) + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: '1', + state: COMMENT_STATE.archived, + uuid: uuidv4(), + authorId: '1', + }, + }) + await atomService.create({ + table: 'comment', + data: { + type: 'article', + targetId: '1', + targetTypeId, + parentCommentId: '1', + state: COMMENT_STATE.banned, + uuid: uuidv4(), + authorId: '1', + }, + }) + + const [_, count2] = await commentService.findByParent({ id: '1' }) + expect(count2).toBe(count) }) }) @@ -199,3 +234,69 @@ describe('find comments', () => { ) }) }) + +test('count comments', async () => { + const { id: targetTypeId } = await atomService.findFirst({ + table: 'entity_type', + where: { table: 'article' }, + }) + + const originalCount = await commentService.countByArticle('1') + + // archived/banned comments should be filtered + await atomService.create({ + table: 'comment', + data: { + type: COMMENT_TYPE.article, + targetId: '1', + targetTypeId, + parentCommentId: null, + state: COMMENT_STATE.archived, + uuid: uuidv4(), + authorId: '1', + }, + }) + await atomService.create({ + table: 'comment', + data: { + type: COMMENT_TYPE.article, + targetId: '1', + targetTypeId, + parentCommentId: null, + state: COMMENT_STATE.banned, + uuid: uuidv4(), + authorId: '1', + }, + }) + + const count1 = await commentService.countByArticle('1') + expect(count1).toBe(originalCount) + + // active/collapsed comments should be included + await atomService.create({ + table: 'comment', + data: { + type: COMMENT_TYPE.article, + targetId: '1', + targetTypeId, + parentCommentId: null, + state: COMMENT_STATE.active, + uuid: uuidv4(), + authorId: '1', + }, + }) + await atomService.create({ + table: 'comment', + data: { + type: COMMENT_TYPE.article, + targetId: '1', + targetTypeId, + parentCommentId: null, + state: COMMENT_STATE.collapsed, + uuid: uuidv4(), + authorId: '1', + }, + }) + const count2 = await commentService.countByArticle('1') + expect(count2).toBe(originalCount + 2) +}) diff --git a/src/connectors/__test__/tagService.test.ts b/src/connectors/__test__/tagService.test.ts index 1d077ce30..4c89cc5f9 100644 --- a/src/connectors/__test__/tagService.test.ts +++ b/src/connectors/__test__/tagService.test.ts @@ -1,15 +1,17 @@ import type { Connections } from 'definitions' -import { TagService } from 'connectors' +import { TagService, AtomService } from 'connectors' import { genConnections, closeConnections } from './utils' let connections: Connections let tagService: TagService +let atomService: AtomService beforeAll(async () => { connections = await genConnections() tagService = new TagService(connections) + atomService = new AtomService(connections) }, 30000) afterAll(async () => { @@ -21,9 +23,57 @@ test('countArticles', async () => { expect(count).toBeDefined() }) -test('findArticleIds', async () => { - const articleIds = await tagService.findArticleIds({ id: '2' }) - expect(articleIds).toBeDefined() +describe('findArticleIds', () => { + test('id', async () => { + const articleIds = await tagService.findArticleIds({ id: '2' }) + expect(articleIds).toBeDefined() + }) + test('excludeRestricted', async () => { + const articleIds = await tagService.findArticleIds({ + id: '2', + excludeRestricted: true, + }) + expect(articleIds).toBeDefined() + + // create a restricted article + await atomService.create({ + table: 'article_recommend_setting', + data: { articleId: articleIds[0], inNewest: true }, + }) + const excluded1 = await tagService.findArticleIds({ + id: '2', + excludeRestricted: true, + }) + expect(excluded1).not.toContain(articleIds[0]) + + // create a non-restricted article with record in article_recommend_setting + await atomService.deleteMany({ table: 'article_recommend_setting' }) + await atomService.create({ + table: 'article_recommend_setting', + data: { articleId: articleIds[0], inNewest: false, inHottest: false }, + }) + const excluded2 = await tagService.findArticleIds({ + id: '2', + excludeRestricted: true, + }) + expect(excluded2).toContain(articleIds[0]) + + // create a restricted user + await atomService.deleteMany({ table: 'article_recommend_setting' }) + const article = await atomService.findUnique({ + table: 'article', + where: { id: articleIds[0] }, + }) + await atomService.create({ + table: 'user_restriction', + data: { userId: article?.authorId, type: 'articleNewest' }, + }) + const excluded3 = await tagService.findArticleIds({ + id: '2', + excludeRestricted: true, + }) + expect(excluded3).not.toContain(articleIds[0]) + }) }) test('findArticleCovers', async () => { diff --git a/src/connectors/__test__/userService.test.ts b/src/connectors/__test__/userService.test.ts index 699322a69..7cc3fdaf9 100644 --- a/src/connectors/__test__/userService.test.ts +++ b/src/connectors/__test__/userService.test.ts @@ -7,6 +7,7 @@ import { CacheService, UserService, PaymentService, + ArticleService, } from 'connectors' import { createDonationTx } from './utils' @@ -17,12 +18,14 @@ let connections: Connections let atomService: AtomService let userService: UserService let paymentService: PaymentService +let articleService: ArticleService beforeAll(async () => { connections = await genConnections() atomService = new AtomService(connections) userService = new UserService(connections) paymentService = new PaymentService(connections) + articleService = new ArticleService(connections) }, 30000) afterAll(async () => { @@ -677,13 +680,59 @@ describe('recommendAuthors', () => { expect(author.userName).not.toBe(null) } }) + test('exclude restircted users', async () => { + const authors = await userService.recommendAuthors({ oss: true }) + await atomService.create({ + table: 'user_restriction', + data: { userId: authors[0].id, type: 'articleHottest' }, + }) + const excluded = await userService.recommendAuthors({ oss: true }) + expect(excluded).not.toContain(authors[0]) + }) + test('exclude archived articles when calculating authorScore', async () => { + const authorId = '1' + const [article] = await articleService.createArticle({ + authorId, + title: 'test', + content: 'test', + }) + await createDonationTx( + { recipientId: authorId, senderId: '2', targetId: article.id }, + paymentService + ) + await connections.knex('article_read_count').insert({ + articleId: article.id, + count: 1, + timedCount: 1, + readTime: 100, + userId: '2', + }) + + const authors = await userService.recommendAuthors({ oss: true }) + for (const author of authors) { + if (author.id === authorId) { + expect(author.authorScore).toBeGreaterThan(0) + } + } + await articleService.archive(article.id) + + const authors2 = await userService.recommendAuthors({ oss: true }) + for (const author of authors) { + if (author.id === authorId) { + expect(authors2[0].authorScore).toBe(0) + } + } + }) }) describe('updateUserExtra', () => { const userId = '1' test('set extra jsonb column', async () => { - let user = await userService.baseFindById(userId) + let user = await atomService.findUnique({ + table: 'user', + where: { id: userId }, + }) expect(user.extra).toBeNull() const referralCode = 'code1' diff --git a/src/connectors/__test__/utils.ts b/src/connectors/__test__/utils.ts index 929693585..9cbe16045 100644 --- a/src/connectors/__test__/utils.ts +++ b/src/connectors/__test__/utils.ts @@ -38,9 +38,11 @@ export const createDonationTx = async ( { senderId, recipientId, + targetId, }: { senderId: string recipientId: string + targetId?: string }, paymentService: PaymentService ) => @@ -51,6 +53,7 @@ export const createDonationTx = async ( purpose: TRANSACTION_PURPOSE.donation, currency: PAYMENT_CURRENCY.HKD, state: TRANSACTION_STATE.succeeded, + targetId, }, paymentService ) @@ -62,12 +65,14 @@ export const createTx = async ( purpose, currency, state, + targetId, }: { senderId: string recipientId: string purpose: TRANSACTION_PURPOSE currency: keyof typeof PAYMENT_CURRENCY state: TRANSACTION_STATE + targetId?: string }, paymentService: PaymentService ) => { @@ -81,7 +86,7 @@ export const createTx = async ( providerTxId: String(Math.random()), recipientId, senderId, - targetId: '1', + targetId: targetId ?? '1', targetType: TRANSACTION_TARGET_TYPE.article, }) } diff --git a/src/connectors/articleService.ts b/src/connectors/articleService.ts index f556df2b0..8cf8b1d5e 100644 --- a/src/connectors/articleService.ts +++ b/src/connectors/articleService.ts @@ -220,15 +220,8 @@ export class ArticleService extends BaseService
{ } } - public loadLatestArticleVersion = async (articleId: string) => { - const version = await this.latestArticleVersionLoader.load(articleId) - return version - ? version - : await this.knexRO('draft') - .where({ articleId }) - .orderBy('id', 'desc') - .first() - } + public loadLatestArticleVersion = (articleId: string) => + this.latestArticleVersionLoader.load(articleId) public loadLatestArticleContent = async (articleId: string) => { const { contentId } = await this.latestArticleVersionLoader.load(articleId) @@ -627,6 +620,7 @@ export class ArticleService extends BaseService
{ columns = ['*'], orderBy = 'newest', state = 'active', + excludeRestricted, skip, take, }: { @@ -638,6 +632,7 @@ export class ArticleService extends BaseService
{ | 'mostAppreciations' | 'mostComments' | 'mostDonations' + excludeRestricted?: boolean skip?: number take?: number } = {} @@ -656,6 +651,15 @@ export class ArticleService extends BaseService
{ if (state) { builder.andWhere({ 't1.state': state }) } + if (excludeRestricted) { + builder.whereNotIn( + 't1.id', + this.knexRO('article_recommend_setting') + .select('articleId') + .where({ inHottest: true }) + .orWhere({ inNewest: true }) + ) + } switch (orderBy) { case 'newest': { diff --git a/src/connectors/atomService.ts b/src/connectors/atomService.ts index 6be1def48..c2a6d1db4 100644 --- a/src/connectors/atomService.ts +++ b/src/connectors/atomService.ts @@ -1,63 +1,19 @@ import type { - ActionArticle, - ActionCircle, - ActionTag, - ActionUser, - Announcement, - AnnouncementTranslation, - Appreciation, Article, - ArticleBoost, - ArticleCircle, - ArticleConnection, ArticleContent, - ArticleCountView, - ArticleReadTimeMaterialized, - ArticleRecommendSetting, - ArticleTag, - ArticleTranslation, ArticleVersion, - Asset, - AssetMap, - BlockchainSyncRecord, - BlockchainTransaction, - BlockedSearchKeyword, - Blocklist, Circle, - CircleInvitation, - CirclePrice, - CircleSubscription, - CircleSubscriptionItem, Collection, - CollectionArticle, Comment, Connections, - CryptoWallet, - CryptoWalletSignature, - Customer, Draft, - EntityType, - FeaturedCommentMaterialized, - MattersChoice, MattersChoiceTopic, - PayoutAccount, - PunishRecord, - RecommendedArticlesFromReadTagsMaterialized, - Report, - SearchHistory, - SeedingUser, TableName, Tag, - TagTranslation, Transaction, User, - UserBadge, - UserIpnsKeys, - UserOauthLikecoinDB, - UserRestriction, - UserTagsOrder, - UsernameEditHistory, - VerificationCode, + TableTypeMap, + TableTypeMapKey, } from 'definitions' import type { Knex } from 'knex' @@ -71,68 +27,6 @@ import { type Mode = 'id' | 'uuid' -type TableTypeMap = { - announcement: Announcement - announcement_translation: AnnouncementTranslation - blocked_search_keyword: BlockedSearchKeyword - blocklist: Blocklist - matters_choice: MattersChoice - user: User - user_ipns_keys: UserIpnsKeys - username_edit_history: UsernameEditHistory - user_restriction: UserRestriction - asset: Asset - asset_map: AssetMap - draft: Draft - article: Article - article_version: ArticleVersion - article_content: ArticleContent - article_circle: ArticleCircle - article_translation: ArticleTranslation - article_tag: ArticleTag - article_boost: ArticleBoost - article_connection: ArticleConnection - article_recommend_setting: ArticleRecommendSetting - article_count_view: ArticleCountView - article_read_time_materialized: ArticleReadTimeMaterialized - collection: Collection - collection_article: CollectionArticle - comment: Comment - featured_comment_materialized: FeaturedCommentMaterialized - action_user: ActionUser - action_article: ActionArticle - action_circle: ActionCircle - action_tag: ActionTag - circle: Circle - circle_price: CirclePrice - circle_invitation: CircleInvitation - circle_subscription: CircleSubscription - circle_subscription_item: CircleSubscriptionItem - customer: Customer - crypto_wallet: CryptoWallet - crypto_wallet_signature: CryptoWalletSignature - tag: Tag - tag_translation: TagTranslation - user_tags_order: UserTagsOrder - verification_code: VerificationCode - punish_record: PunishRecord - search_history: SearchHistory - payout_account: PayoutAccount - transaction: Transaction - blockchain_transaction: BlockchainTransaction - blockchain_sync_record: BlockchainSyncRecord - entity_type: EntityType - appreciation: Appreciation - seeding_user: SeedingUser - user_oauth_likecoin: UserOauthLikecoinDB - user_badge: UserBadge - report: Report - recommended_articles_from_read_tags_materialized: RecommendedArticlesFromReadTagsMaterialized - matters_choice_topic: MattersChoiceTopic -} - -type TableTypeMapKey = keyof TableTypeMap - interface InitLoaderInput { table: TableTypeMapKey mode: Mode diff --git a/src/connectors/commentService.ts b/src/connectors/commentService.ts index 4dd5c9e58..850f4cc03 100644 --- a/src/connectors/commentService.ts +++ b/src/connectors/commentService.ts @@ -31,14 +31,16 @@ export class CommentService extends BaseService { /** * Count comments by a given article id. + * + * @remarks only count active and collapsed comments */ public countByArticle = async (articleId: string) => { - const result = await this.knex(this.table) + const result = await this.knexRO(this.table) .where({ targetId: articleId, - state: COMMENT_STATE.active, type: COMMENT_TYPE.article, }) + .whereIn('state', [COMMENT_STATE.active, COMMENT_STATE.collapsed]) .count() .first() return parseInt(result ? (result.count as string) : '0', 10) @@ -46,6 +48,8 @@ export class CommentService extends BaseService { /** * Find comments by a given comment id. + * + * @remarks only find active and collapsed comments */ public findByParent = async ({ id, @@ -64,10 +68,11 @@ export class CommentService extends BaseService { let query = null const sortCreatedAt = (by: 'desc' | 'asc') => - this.knex + this.knexRO .select(['*', this.knex.raw('count(1) OVER() AS total_count')]) .from(this.table) .where(where) + .whereIn('state', [COMMENT_STATE.active, COMMENT_STATE.collapsed]) .orderBy('created_at', by) if (author) { diff --git a/src/connectors/queue/migration.ts b/src/connectors/queue/migration.ts index a1e736190..0421bab1a 100644 --- a/src/connectors/queue/migration.ts +++ b/src/connectors/queue/migration.ts @@ -126,7 +126,10 @@ export class MigrationQueue extends BaseQueue { content: content && normalizeArticleHTML( - sanitizeHTML(content, { maxEmptyParagraphs: -1 }) + sanitizeHTML(content, { + maxHardBreaks: -1, + maxSoftBreaks: -1, + }) ), }) diff --git a/src/connectors/queue/payTo/blockchain.ts b/src/connectors/queue/payTo/blockchain.ts index 463fb4019..8da774be9 100644 --- a/src/connectors/queue/payTo/blockchain.ts +++ b/src/connectors/queue/payTo/blockchain.ts @@ -182,15 +182,16 @@ export class PayToByBlockchainQueue extends BaseQueue { where: { id: tx.targetId }, }), ]) - const articleService = new ArticleService(this.connections) - const articleVersion = await articleService.loadLatestArticleVersion( - article.id - ) + const articleVersions = await atomService.findMany({ + table: 'article_version', + where: { articleId: article.id }, + }) + const articleCids = articleVersions.map((v) => v.dataHash) // cancel tx and success blockchain tx if it's invalid // Note: sender and recipient's ETH address may change after tx is created const isValidTx = await this.containMatchedEvent(txReceipt.events, { - cid: articleVersion.dataHash, + cids: articleCids, amount: tx.amount, // support USDT only for now tokenAddress: contract[chain].tokenAddress, @@ -591,12 +592,12 @@ export class PayToByBlockchainQueue extends BaseQueue { private containMatchedEvent = async ( events: CurationEvent[], { - cid, + cids, tokenAddress, amount, decimals, }: { - cid: string + cids: string[] tokenAddress: string amount: string decimals: number @@ -611,7 +612,7 @@ export class PayToByBlockchainQueue extends BaseQueue { ignoreCaseMatch(event.tokenAddress || '', tokenAddress) && event.amount === parseUnits(amount, decimals).toString() && isValidUri(event.uri) && - extractCid(event.uri) === cid + cids.includes(extractCid(event.uri)) ) { return true } diff --git a/src/connectors/systemService.ts b/src/connectors/systemService.ts index 25a79d365..89379f061 100644 --- a/src/connectors/systemService.ts +++ b/src/connectors/systemService.ts @@ -37,6 +37,7 @@ export class SystemService extends BaseService { private featureFlagTable: string public constructor(connections: Connections) { + // @ts-ignore super('noop', connections) this.featureFlagTable = 'feature_flag' diff --git a/src/connectors/tagService.ts b/src/connectors/tagService.ts index 276a91844..badcb7bb7 100644 --- a/src/connectors/tagService.ts +++ b/src/connectors/tagService.ts @@ -941,23 +941,23 @@ export class TagService extends BaseService { selected, sortBy, withSynonyms, + excludeRestricted, skip, take, }: { id: string - // filter?: { [key: string]: any } selected?: boolean sortBy?: 'byHottestDesc' | 'byCreatedAtDesc' withSynonyms?: boolean + excludeRestricted?: boolean skip?: number take?: number }) => { - const results = await this.knex + const results = await this.knexRO .select('article_id') .from('article_tag') .join('article', 'article_id', 'article.id') .where({ - // tagId, state: ARTICLE_STATE.active, ...(selected === true ? { selected } : {}), }) @@ -966,7 +966,7 @@ export class TagService extends BaseService { if (withSynonyms) { builder.orWhereIn( 'tag_id', - this.knex + this.knexRO .from(MATERIALIZED_VIEW.tags_lasts_view_materialized) .whereRaw('dup_tag_ids @> ARRAY[?] ::int[]', tagId) .select(this.knex.raw('UNNEST(dup_tag_ids)')) @@ -974,6 +974,21 @@ export class TagService extends BaseService { } }) .modify((builder: Knex.QueryBuilder) => { + if (excludeRestricted) { + builder + .whereNotIn( + 'article.id', + this.knexRO + .select('articleId') + .from('article_recommend_setting') + .where({ inHottest: true }) + .orWhere({ inNewest: true }) + ) + .whereNotIn( + 'article.authorId', + this.knexRO.select('userId').from('user_restriction') + ) + } if (sortBy === 'byHottestDesc') { builder .join( diff --git a/src/connectors/userService.ts b/src/connectors/userService.ts index 7641d3355..2ca111c0e 100644 --- a/src/connectors/userService.ts +++ b/src/connectors/userService.ts @@ -63,7 +63,6 @@ import { VERIFICATION_CODE_TYPE, USER_RESTRICTION_TYPE, VIEW, - AUTO_FOLLOW_TAGS, CIRCLE_STATE, DB_NOTICE_TYPE, INVITATION_STATE, @@ -112,7 +111,6 @@ import { AtomService, BaseService, CacheService, - TagService, ipfsServers, OAuthService, NotificationService, @@ -229,10 +227,6 @@ export class UserService extends BaseService { // auto follow matty await this.follow(user.id, environment.mattyId) - // auto follow tags - const tagService = new TagService(this.connections) - await tagService.followTags(user.id, AUTO_FOLLOW_TAGS) - // send email if (user.email && user.displayName) { notificationService.mail.sendRegisterSuccess({ @@ -1284,34 +1278,18 @@ export class UserService extends BaseService { type?: keyof typeof AUTHOR_TYPE count?: boolean }) => { + let query: Knex.QueryBuilder switch (type) { case AUTHOR_TYPE.default: { - const table = oss + const view = oss ? VIEW.user_reader_view : MATERIALIZED_VIEW.user_reader_materialized - const query = this.knexRO(table) + query = this.knexRO + .from({ view }) .orderByRaw('author_score DESC NULLS LAST') .orderBy('id', 'desc') - .where({ state: USER_STATE.active }) .whereNot({ userName: null }) - .whereNotIn('id', notIn) - - if (skip) { - query.offset(skip) - } - if (take || take === 0) { - query.limit(take) - } - if (count) { - query.select( - '*', - this.knexRO.raw('COUNT(id) OVER() ::int AS total_count') - ) - } else { - query.select('*') - } - - return query + break } case AUTHOR_TYPE.active: case AUTHOR_TYPE.appreciated: @@ -1323,30 +1301,30 @@ export class UserService extends BaseService { ? 'most_appreciated_author_materialized' : 'most_trendy_author_materialized' - const query = this.knexRO + query = this.knexRO .from({ view }) .innerJoin('user', 'view.id', 'user.id') - .where({ state: USER_STATE.active }) - .whereNotIn('view.id', notIn) - - if (skip) { - query.offset(skip) - } - if (take || take === 0) { - query.limit(take) - } - if (count) { - query.select( - '*', - this.knexRO.raw('COUNT(id) OVER() ::int AS total_count') - ) - } else { - query.select('*') - } - - return query } } + query + .where({ state: USER_STATE.active }) + .whereNotIn('view.id', notIn) + .whereNotIn('view.id', this.knexRO('user_restriction').select('user_id')) + if (skip) { + query.offset(skip) + } + if (take || take === 0) { + query.limit(take) + } + if (count) { + query.select( + '*', + this.knexRO.raw('COUNT(id) OVER() ::int AS total_count') + ) + } else { + query.select('*') + } + return query } public findBoost = async (userId: string) => { @@ -2073,6 +2051,11 @@ export class UserService extends BaseService { return ipnsKeyRec } + if (!user.ethAddress) { + // stop create IPNS for users without wallet + return + } + // create it if not existed const kname = `for-${user.userName}-${user.uuid}` const { diff --git a/src/definitions/appreciation.ts b/src/definitions/appreciation.d.ts similarity index 100% rename from src/definitions/appreciation.ts rename to src/definitions/appreciation.d.ts diff --git a/src/definitions/base.d.ts b/src/definitions/base.d.ts index f1666b105..442538f53 100644 --- a/src/definitions/base.d.ts +++ b/src/definitions/base.d.ts @@ -1,3 +1,8 @@ export interface BaseDBSchema { id: string } + +export interface EntityType { + id: string + table: TableName +} diff --git a/src/definitions/index.d.ts b/src/definitions/index.d.ts index 18a581d32..808a6d087 100644 --- a/src/definitions/index.d.ts +++ b/src/definitions/index.d.ts @@ -1,20 +1,83 @@ +import type { + ActionArticle, + ActionCircle, + ActionTag, + ActionUser, +} from './action' +import type { Announcement, AnnouncementTranslation } from './announcement' +import type { Appreciation } from './appreciation' +import type { + Article, + ArticleBoost, + ArticleCircle, + ArticleConnection, + ArticleContent, + ArticleCountView, + ArticleReadTimeMaterialized, + ArticleRecommendSetting, + ArticleTag, + ArticleTranslation, + ArticleVersion, + RecommendedArticlesFromReadTagsMaterialized, +} from './article' +import type { Asset, AssetMap } from './asset' +import type { VerificationCode } from './auth' +import type { EntityType } from './base' +import type { + Circle, + CircleInvitation, + CirclePrice, + CircleSubscription, + CircleSubscriptionItem, +} from './circle' +import type { Collection, CollectionArticle } from './collection' +import type { Comment, FeaturedCommentMaterialized } from './comment' +import type { Draft } from './draft' +import type { + BlockedSearchKeyword, + Blocklist, + MattersChoice, + MattersChoiceTopic, + PunishRecord, + SearchHistory, +} from './misc' +import type { UserOauthLikecoinDB } from './oauth' +import type { + BlockchainSyncRecord, + BlockchainTransaction, + Customer, + PayoutAccount, + Transaction, +} from './payment' +import type { Report } from './report' +import type { Tag, TagTranslation, UserTagsOrder } from './tag' +import type { + SeedingUser, + User, + UserBadge, + UserIpnsKeys, + UserIpnsKeys, + UserRestriction, + UsernameEditHistory, +} from './user' +import type { CryptoWallet, CryptoWalletSignature } from './wallet' import type { BasedContext } from '@apollo/server' import type { ArticleService, AtomService, + CollectionService, CommentService, DraftService, + ExchangeRate, + LikeCoin, NotificationService, OAuthService, OpenSeaService, PaymentService, + RecommendationService, SystemService, TagService, UserService, - CollectionService, - LikeCoin, - ExchangeRate, - RecommendationService, } from 'connectors' import type { PublicationQueue, @@ -96,88 +159,86 @@ export interface DataSources { } } -export type BasicTableName = - | 'action' - | 'article_boost' - | 'action_user' +export type TableTypeMap = { + action_article: ActionArticle + action_circle: ActionCircle + action_tag: ActionTag + action_user: ActionUser + announcement: Announcement + announcement_translation: AnnouncementTranslation + appreciation: Appreciation + article: Article + article_boost: ArticleBoost + article_circle: ArticleCircle + article_connection: ArticleConnection + article_content: ArticleContent + article_count_view: ArticleCountView + article_read_time_materialized: ArticleReadTimeMaterialized + article_recommend_setting: ArticleRecommendSetting + article_tag: ArticleTag + article_translation: ArticleTranslation + article_version: ArticleVersion + asset: Asset + asset_map: AssetMap + blockchain_sync_record: BlockchainSyncRecord + blockchain_transaction: BlockchainTransaction + blocked_search_keyword: BlockedSearchKeyword + blocklist: Blocklist + circle: Circle + circle_invitation: CircleInvitation + circle_price: CirclePrice + circle_subscription: CircleSubscription + circle_subscription_item: CircleSubscriptionItem + collection: Collection + collection_article: CollectionArticle + comment: Comment + crypto_wallet: CryptoWallet + crypto_wallet_signature: CryptoWalletSignature + customer: Customer + draft: Draft + entity_type: EntityType + featured_comment_materialized: FeaturedCommentMaterialized + matters_choice: MattersChoice + matters_choice_topic: MattersChoiceTopic + payout_account: PayoutAccount + punish_record: PunishRecord + recommended_articles_from_read_tags_materialized: RecommendedArticlesFromReadTagsMaterialized + report: Report + search_history: SearchHistory + seeding_user: SeedingUser + tag: Tag + tag_translation: TagTranslation + transaction: Transaction + user: User + user_badge: UserBadge + user_ipns_keys: UserIpnsKeys + user_oauth_likecoin: UserOauthLikecoinDB + user_restriction: UserRestriction + user_tags_order: UserTagsOrder + username_edit_history: UsernameEditHistory + verification_code: VerificationCode +} + +export type TableTypeMapKey = keyof TableTypeMap + +// table not in TableTypeMap +type OtherTable = | 'action_comment' - | 'action_article' - | 'action_tag' - | 'transaction' - | 'appreciation' - | 'asset' - | 'asset_map' - | 'article' | 'article_read_count' - | 'article_tag' - | 'audio_draft' - | 'comment' - | 'article_connection' - | 'draft' - | 'noop' - | 'user' - | 'user_oauth' - | 'user_badge' - | 'user_notify_setting' - | 'user_restriction' - | 'username_edit_history' - | 'notice_detail' - | 'notice' - | 'notice_actor' - | 'notice_entity' - | 'push_device' - | 'report' + | 'blockchain_curation_event' | 'feedback' - | 'feedback_asset' - | 'invitation' - | 'verification_code' - | 'search_history' - | 'tag' - | 'tag_boost' - | 'user_boost' - | 'matters_today' - | 'matters_choice' - | 'matters_choice_tag' - | 'article_recommend_setting' | 'log_record' - | 'oauth_client' + | 'matters_choice_tag' + | 'notice' | 'oauth_access_token' | 'oauth_authorization_code' + | 'oauth_client' + | 'oauth_refresh_token' | 'oauth_refresh_token' - | 'user_oauth_likecoin' - | 'blocklist' - | 'blocked_search_keyword' - | 'transaction' - | 'customer' - | 'payout_account' - | 'punish_record' - | 'entity_type' - | 'circle' - | 'circle_invitation' - | 'circle_price' - | 'circle_subscription' - | 'circle_subscription_item' - | 'action_circle' - | 'article_circle' - | 'feature_flag' - | 'seeding_user' - | 'announcement' - | 'announcement_translation' - | 'crypto_wallet' - | 'crypto_wallet_signature' - | 'article_translation' - | 'tag_translation' - | 'user_ipns_keys' - | 'user_tags_order' - | 'blockchain_transaction' - | 'blockchain_curation_event' - | 'blockchain_sync_record' - | 'collection' - | 'collection_article' | 'social_account' - | 'article_content' - | 'article_version' - | 'matters_choice_topic' + | 'tag_boost' + | 'user_boost' + | 'user_notify_setting' export type View = | 'tag_count_view' @@ -204,24 +265,7 @@ export type MaterializedView = | 'article_read_time_materialized' | 'recommended_articles_from_read_tags_materialized' -export type TableName = BasicTableName | View | MaterializedView - -export interface EntityType { - id: string - table: TableName -} - -export interface ThirdPartyAccount { - accountName: 'facebook' | 'wechat' | 'google' - baseUrl: string - token: string -} - -export interface BatchParams { - input: { - [key: string]: any - } -} +export type TableName = TableTypeMapKey | View | MaterializedView | OtherTable export type S3Bucket = | 'matters-server-dev' diff --git a/src/mutations/article/appreciateArticle.ts b/src/mutations/article/appreciateArticle.ts index 8a6b43913..85e0344a6 100644 --- a/src/mutations/article/appreciateArticle.ts +++ b/src/mutations/article/appreciateArticle.ts @@ -105,7 +105,7 @@ const resolver: GQLMutationResolvers['appreciateArticle'] = async ( // insert record to LikeCoin const likecoin = new LikeCoin(connections) - if (author.likerId && sender.likerId) { + if (author.likerId && sender.likerId && author.likerId !== sender.likerId) { likecoin.like({ likerId: sender.likerId, likerIp: viewer.ip, diff --git a/src/mutations/article/editArticle.ts b/src/mutations/article/editArticle.ts index 99a3abd61..1921d5535 100644 --- a/src/mutations/article/editArticle.ts +++ b/src/mutations/article/editArticle.ts @@ -1,5 +1,6 @@ import type { Article, Draft, Circle, GQLMutationResolvers } from 'definitions' +import { invalidateFQC } from '@matters/apollo-response-cache' import { stripHtml } from '@matters/ipns-site-generator' import { normalizeArticleHTML, @@ -62,6 +63,7 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( atomService, systemService, queues: { revisionQueue }, + connections: { redis }, }, } ) => { @@ -105,6 +107,11 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( ) } if (state === ARTICLE_STATE.archived) { + // purge author cache, article cache invalidation already in directive + invalidateFQC({ + node: { type: NODE_TYPES.User, id: article.authorId }, + redis, + }) return articleService.archive(dbId) } @@ -310,7 +317,7 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( const { content: lastContent } = await atomService.articleContentIdLoader.load(articleVersion.contentId) const processed = normalizeArticleHTML( - sanitizeHTML(content, { maxEmptyParagraphs: -1 }) + sanitizeHTML(content, { maxHardBreaks: -1, maxSoftBreaks: -1 }) ) const changed = processed !== lastContent diff --git a/src/mutations/comment/putComment.ts b/src/mutations/comment/putComment.ts index 943629d59..e56d896ef 100644 --- a/src/mutations/comment/putComment.ts +++ b/src/mutations/comment/putComment.ts @@ -23,7 +23,6 @@ import { COMMENT_TYPE, DB_NOTICE_TYPE, MAX_ARTICLE_COMMENT_LENGTH, - MAX_COMMENT_EMPTY_PARAGRAPHS, NODE_TYPES, USER_STATE, } from 'common/enums' @@ -76,7 +75,8 @@ const resolver: GQLMutationResolvers['putComment'] = async ( const data: Partial & { mentionedUserIds?: any } = { content: normalizeCommentHTML( sanitizeHTML(content, { - maxEmptyParagraphs: MAX_COMMENT_EMPTY_PARAGRAPHS, + maxHardBreaks: 0, + maxSoftBreaks: 1, }) ), authorId: viewer.id, diff --git a/src/mutations/draft/putDraft.ts b/src/mutations/draft/putDraft.ts index e4ffb8340..88a0213bc 100644 --- a/src/mutations/draft/putDraft.ts +++ b/src/mutations/draft/putDraft.ts @@ -161,7 +161,9 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( summary: summary === null ? null : summary?.trim(), content: content && - normalizeArticleHTML(sanitizeHTML(content, { maxEmptyParagraphs: -1 })), + normalizeArticleHTML( + sanitizeHTML(content, { maxHardBreaks: -1, maxSoftBreaks: -1 }) + ), tags: tags?.length === 0 ? null : tags, cover: coverId, collection: collection?.length === 0 ? null : collection, diff --git a/src/mutations/system/putRestrictedUsers.ts b/src/mutations/system/putRestrictedUsers.ts index 062f59c5d..73b628039 100644 --- a/src/mutations/system/putRestrictedUsers.ts +++ b/src/mutations/system/putRestrictedUsers.ts @@ -1,12 +1,22 @@ import type { GQLMutationResolvers } from 'definitions' +import { invalidateFQC } from '@matters/apollo-response-cache' + +import { NODE_TYPES } from 'common/enums' import { UserInputError } from 'common/errors' import { fromGlobalId } from 'common/utils' const resolver: GQLMutationResolvers['putRestrictedUsers'] = async ( _, { input: { ids, restrictions } }, - { dataSources: { userService } } + { + dataSources: { + userService, + atomService, + articleService, + connections: { redis }, + }, + } ) => { if (ids.length === 0) { throw new UserInputError('"ids" is required') @@ -16,9 +26,16 @@ const resolver: GQLMutationResolvers['putRestrictedUsers'] = async ( for (const userId of userIds) { await userService.updateRestrictions(userId, restrictions) + invalidateFQC({ node: { id: userId, type: NODE_TYPES.User }, redis }) + for (const article of await articleService.findByAuthor(userId)) { + invalidateFQC({ + node: { id: article.id, type: NODE_TYPES.Article }, + redis, + }) + } } - return userService.baseFindByIds(userIds) + return atomService.userIdLoader.loadMany(userIds) } export default resolver diff --git a/src/mutations/user/refreshIPNSFeed.ts b/src/mutations/user/refreshIPNSFeed.ts index 1925771d2..3ea2d5f6c 100644 --- a/src/mutations/user/refreshIPNSFeed.ts +++ b/src/mutations/user/refreshIPNSFeed.ts @@ -10,14 +10,15 @@ const resolver: GQLMutationResolvers['refreshIPNSFeed'] = async ( }, } ) => { - // const ipnsKeyRec = - await userService.findOrCreateIPNSKey(userName) + const ipnsKeyRec = await userService.findOrCreateIPNSKey(userName) - publicationQueue.refreshIPNSFeed({ - userName, - numArticles, - forceReplace: true, - }) + if (ipnsKeyRec) { + publicationQueue.refreshIPNSFeed({ + userName, + numArticles, + forceReplace: true, + }) + } return userService.findByUserName(userName) } diff --git a/src/mutations/user/updateUserState.ts b/src/mutations/user/updateUserState.ts index 610b8f94e..8aa5a1ac5 100644 --- a/src/mutations/user/updateUserState.ts +++ b/src/mutations/user/updateUserState.ts @@ -13,7 +13,6 @@ const resolver: GQLMutationResolvers['updateUserState'] = async ( userService, notificationService, atomService, - connections: { knex }, queues: { userQueue }, }, } diff --git a/src/mutations/user/walletLogin.ts b/src/mutations/user/walletLogin.ts index b25ce924f..ef1d7d0e3 100644 --- a/src/mutations/user/walletLogin.ts +++ b/src/mutations/user/walletLogin.ts @@ -5,11 +5,9 @@ import type { User, } from 'definitions' -import { invalidateFQC } from '@matters/apollo-response-cache' import { Hex } from 'viem' import { - NODE_TYPES, VERIFICATION_CODE_STATUS, VERIFICATION_CODE_TYPE, AUTH_RESULT_TYPE, @@ -88,12 +86,7 @@ const _walletLogin: Exclude< viewer, req, res, - dataSources: { - userService, - atomService, - systemService, - connections: { redis }, - }, + dataSources: { userService, atomService, systemService }, } = context const lastSigning = await userService.verifyWalletSignature({ @@ -108,34 +101,6 @@ const _walletLogin: Exclude< ], }) - /** - * Link - */ - if (viewer.id && viewer.token && !viewer.ethAddress) { - await atomService.update({ - table: sigTable, - where: { id: lastSigning.id }, - data: { - signature, - userId: viewer.id, - }, - }) - - await userService.addWallet(viewer.id, ethAddress) - - await invalidateFQC({ - node: { type: NODE_TYPES.User, id: viewer.id }, - redis, - }) - - return { - token: viewer.token, - auth: true, - type: AUTH_RESULT_TYPE.LinkAccount, - user: viewer, - } - } - const archivedCallback = async () => systemService.saveAgentHash(viewer.agentHash || '') diff --git a/src/queries/article/contents/html.ts b/src/queries/article/contents/html.ts index 3209319b3..311f35bb0 100644 --- a/src/queries/article/contents/html.ts +++ b/src/queries/article/contents/html.ts @@ -3,8 +3,7 @@ import type { GQLArticleContentsResolvers } from 'definitions' import { ARTICLE_ACCESS_TYPE, ARTICLE_STATE } from 'common/enums' export const html: GQLArticleContentsResolvers['html'] = async ( - // @ts-ignore - { articleId, contentId, content: draftContent }, + { articleId, contentId }, _, { viewer, dataSources: { articleService, paymentService, atomService } } ) => { @@ -15,10 +14,7 @@ export const html: GQLArticleContentsResolvers['html'] = async ( // check viewer if (isAdmin || isAuthor) { - return ( - draftContent ?? - (await atomService.articleContentIdLoader.load(contentId)).content - ) + return (await atomService.articleContentIdLoader.load(contentId)).content } // check article state @@ -30,20 +26,14 @@ export const html: GQLArticleContentsResolvers['html'] = async ( // not in circle if (!articleCircle) { - return ( - draftContent ?? - (await atomService.articleContentIdLoader.load(contentId)).content - ) + return (await atomService.articleContentIdLoader.load(contentId)).content } const isPublic = articleCircle.access === ARTICLE_ACCESS_TYPE.public // public if (isPublic) { - return ( - draftContent ?? - (await atomService.articleContentIdLoader.load(contentId)).content - ) + return (await atomService.articleContentIdLoader.load(contentId)).content } if (!viewer.id) { @@ -60,8 +50,5 @@ export const html: GQLArticleContentsResolvers['html'] = async ( return '' } - return ( - draftContent ?? - (await atomService.articleContentIdLoader.load(contentId)).content - ) + return (await atomService.articleContentIdLoader.load(contentId)).content } diff --git a/src/queries/article/language.ts b/src/queries/article/language.ts index bce6b4c44..ee4d875b1 100644 --- a/src/queries/article/language.ts +++ b/src/queries/article/language.ts @@ -13,7 +13,6 @@ const resolver: GQLArticleResolvers['language'] = async ( id: versionId, language: storedLanguage, contentId, - content: draftContent, } = await articleService.loadLatestArticleVersion(articleId) if (storedLanguage) { return storedLanguage @@ -21,9 +20,7 @@ const resolver: GQLArticleResolvers['language'] = async ( const gcp = new GCP() - const content = draftContent - ? draftContent - : (await atomService.articleContentIdLoader.load(contentId)).content + const { content } = await atomService.articleContentIdLoader.load(contentId) gcp.detectLanguage(stripHtml(content).slice(0, 300)).then((language) => { language && diff --git a/src/queries/article/relatedArticles.ts b/src/queries/article/relatedArticles.ts index ff4e866dc..5d543a6b9 100644 --- a/src/queries/article/relatedArticles.ts +++ b/src/queries/article/relatedArticles.ts @@ -40,6 +40,7 @@ const resolver: GQLArticleResolvers['relatedArticles'] = async ( const articleIds = await tagService.findArticleIds({ id: tagId, + excludeRestricted: true, take, skip, }) @@ -60,6 +61,7 @@ const resolver: GQLArticleResolvers['relatedArticles'] = async ( const articlesFromAuthor = await articleService.findByAuthor(authorId, { skip: 3, state: ARTICLE_STATE.active, + excludeRestricted: true, }) articles = addRec(articles, articlesFromAuthor) } diff --git a/src/queries/article/summary.ts b/src/queries/article/summary.ts index 99f8541c9..bb1618280 100644 --- a/src/queries/article/summary.ts +++ b/src/queries/article/summary.ts @@ -9,12 +9,8 @@ const resolver: GQLArticleResolvers['summary'] = async ( _, { dataSources: { articleService, atomService } } ) => { - const { - summary, - contentId, - summaryCustomized, - content: draftContent, - } = await articleService.loadLatestArticleVersion(id) + const { summary, contentId, summaryCustomized } = + await articleService.loadLatestArticleVersion(id) const accessType = await articleService.getAccess(id) @@ -25,10 +21,6 @@ const resolver: GQLArticleResolvers['summary'] = async ( return summary || '' } - if (draftContent) { - return summary || makeSummary(draftContent) - } - const { content } = await atomService.articleContentIdLoader.load(contentId) return summary || makeSummary(content) } diff --git a/src/queries/draft/article/drafts.ts b/src/queries/draft/article/drafts.ts index 72dd7997e..8b1812bdf 100644 --- a/src/queries/draft/article/drafts.ts +++ b/src/queries/draft/article/drafts.ts @@ -1,32 +1,14 @@ import type { GQLArticleResolvers } from 'definitions' const resolver: GQLArticleResolvers['drafts'] = async ( - { id: articleId, authorId }, + { id: articleId }, _, { dataSources: { atomService } } -) => { - const versions = await atomService.findMany({ - table: 'article_version', +) => + atomService.findMany({ + table: 'draft', where: { articleId }, orderBy: [{ column: 'created_at', order: 'desc' }], }) - return versions.map(async (version) => { - return { - ...version, - authorId, - content: ( - await atomService.articleContentIdLoader.load(version.contentId) - ).content, - publishState: 'published', - // unused fields in front-end - contentMd: '', - iscnPublish: false, - collection: null, - remark: null, - archived: false, - language: null, - } - }) -} export default resolver diff --git a/src/queries/draft/article/newestPublishedDraft.ts b/src/queries/draft/article/newestPublishedDraft.ts index 828e96d01..72b4e35b1 100644 --- a/src/queries/draft/article/newestPublishedDraft.ts +++ b/src/queries/draft/article/newestPublishedDraft.ts @@ -1,29 +1,18 @@ import type { GQLArticleResolvers } from 'definitions' +import { PUBLISH_STATE } from 'common/enums' + const resolver: GQLArticleResolvers['newestPublishedDraft'] = async ( - { id: articleId, authorId }, + { id: articleId }, _, { dataSources: { atomService } } ) => { - const version = await atomService.findFirst({ - table: 'article_version', - where: { articleId }, + const draft = await atomService.findFirst({ + table: 'draft', + where: { articleId, publishState: PUBLISH_STATE.published }, orderBy: [{ column: 'created_at', order: 'desc' }], }) - return { - ...version, - authorId, - content: (await atomService.articleContentIdLoader.load(version.contentId)) - .content, - publishState: 'published', - // unused fields in front-end - contentMd: '', - iscnPublish: false, - collection: null, - remark: null, - archived: false, - language: null, - } + return draft } export default resolver diff --git a/src/queries/user/latestWorks.ts b/src/queries/user/latestWorks.ts index 901251ac6..252b53121 100644 --- a/src/queries/user/latestWorks.ts +++ b/src/queries/user/latestWorks.ts @@ -23,7 +23,7 @@ const resolver: GQLUserResolvers['latestWorks'] = async ( __type: NODE_TYPES.Collection, })), ] - .sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime()) + .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) .slice(0, LATEST_WORKS_NUM) return works diff --git a/src/queries/user/recommendation/authors.ts b/src/queries/user/recommendation/authors.ts index b8f5daa21..ca217f831 100644 --- a/src/queries/user/recommendation/authors.ts +++ b/src/queries/user/recommendation/authors.ts @@ -45,7 +45,10 @@ export const authors: GQLRecommendationResolvers['authors'] = async ( */ if (isAppreciated) { const trendyAuthors = await userService.recommendAuthors({ take: 60, type }) - notIn = [...notIn, ...trendyAuthors.map((author) => author.id)] + notIn = [ + ...notIn, + ...trendyAuthors.map((author: { id: string }) => author.id), + ] } /** @@ -66,7 +69,7 @@ export const authors: GQLRecommendationResolvers['authors'] = async ( const index = Math.min(filter.random, MAX_RANDOM_INDEX, chunks.length - 1) const filteredAuthors = chunks[index] || [] - return connectionFromArray(filteredAuthors, input, authorPool.length) + return connectionFromArray(filteredAuthors as any, input, authorPool.length) } const users = await userService.recommendAuthors({ @@ -78,5 +81,5 @@ export const authors: GQLRecommendationResolvers['authors'] = async ( }) const totalCount = +users[0]?.totalCount || users.length - return connectionFromPromisedArray(users, input, totalCount) + return connectionFromPromisedArray(users as any, input, totalCount) } diff --git a/src/types/__test__/1/collection.test.ts b/src/types/__test__/1/collection.test.ts index fa431afa3..8014af58b 100644 --- a/src/types/__test__/1/collection.test.ts +++ b/src/types/__test__/1/collection.test.ts @@ -146,6 +146,12 @@ const GET_LATEST_WORKS = /* GraphQL */ ` latestWorks { id title + ... on Article { + revisedAt + } + ... on Collection { + updatedAt + } } } } @@ -839,4 +845,9 @@ test('get latest works', async () => { query: GET_LATEST_WORKS, }) expect(data?.viewer?.latestWorks.length).toBeLessThan(5) + if (data?.viewer?.latestWorks.length >= 2) { + expect(data?.viewer?.latestWorks[0].updatedAt.getTime()).toBeGreaterThan( + data?.viewer?.latestWorks[1].updatedAt.getTime() + ) + } }) diff --git a/src/types/__test__/1/editArticle.test.ts b/src/types/__test__/1/editArticle.test.ts index 1c0b99e4e..009b7d356 100644 --- a/src/types/__test__/1/editArticle.test.ts +++ b/src/types/__test__/1/editArticle.test.ts @@ -1000,7 +1000,11 @@ describe('edit article', () => { // create duplicate article with same content const articleService = new ArticleService(connections) - const article = await articleService.baseFindById(articleDbId) + const atomService = new AtomService(connections) + const article = await atomService.findUnique({ + table: 'article', + where: { id: articleDbId }, + }) const articleVersion = await articleService.loadLatestArticleVersion( article.id ) @@ -1019,7 +1023,7 @@ describe('edit article', () => { }) // archive - const { data: archivedData } = await server.executeOperation({ + const { data: archivedData, errors } = await server.executeOperation({ query: EDIT_ARTICLE, variables: { input: { @@ -1028,6 +1032,7 @@ describe('edit article', () => { }, }, }) + expect(errors).toBeUndefined() expect(archivedData.editArticle.state).toBe(ARTICLE_STATE.archived) // refetch & expect de-duplicated