From c933406aa2227cc51e5a7ce68277ba82a2828816 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 13 May 2024 09:48:23 +0800 Subject: [PATCH 01/17] refactor: rewriting blockUser and util to TS --- package-lock.json | 39 ++++++++++++++++++++++ package.json | 2 ++ src/scripts/{blockUser.js => blockUser.ts} | 18 +++++----- src/util/{client.js => client.ts} | 15 +++++++-- src/util/getAllDocs.ts | 2 +- 5 files changed, 64 insertions(+), 12 deletions(-) rename src/scripts/{blockUser.js => blockUser.ts} (95%) rename src/util/{client.js => client.ts} (71%) diff --git a/package-lock.json b/package-lock.json index 8491f4a0..7af2a75d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,8 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-typescript": "^7.24.1", "@google-cloud/storage": "^6.11.0", + "@types/cli-progress": "^3.11.5", + "@types/dotenv": "^8.2.0", "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", @@ -4500,6 +4502,15 @@ "@types/node": "*" } }, + "node_modules/@types/cli-progress": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", + "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/color-name": { "version": "1.1.1", "dev": true, @@ -4529,6 +4540,16 @@ "@types/express": "*" } }, + "node_modules/@types/dotenv": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.3.tgz", + "integrity": "sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==", + "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "dotenv": "*" + } + }, "node_modules/@types/express": { "version": "4.17.3", "license": "MIT", @@ -20490,6 +20511,15 @@ "@types/node": "*" } }, + "@types/cli-progress": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", + "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "dev": true @@ -20515,6 +20545,15 @@ "@types/express": "*" } }, + "@types/dotenv": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.3.tgz", + "integrity": "sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==", + "dev": true, + "requires": { + "dotenv": "*" + } + }, "@types/express": { "version": "4.17.3", "requires": { diff --git a/package.json b/package.json index 8fed135c..38d80d21 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,8 @@ "@babel/preset-typescript": "^7.24.1", "@google-cloud/storage": "^6.11.0", "@types/node": "^18", + "@types/cli-progress": "^3.11.5", + "@types/dotenv": "^8.2.0", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "apollo-server-testing": "^2.18.2", diff --git a/src/scripts/blockUser.js b/src/scripts/blockUser.ts similarity index 95% rename from src/scripts/blockUser.js rename to src/scripts/blockUser.ts index ad1de5a4..d169b8dc 100644 --- a/src/scripts/blockUser.js +++ b/src/scripts/blockUser.ts @@ -14,10 +14,10 @@ import { updateArticleReplyByFeedbacks } from 'graphql/mutations/CreateOrUpdateA /** * Update user to write blockedReason. Throws if user does not exist. * - * @param {string} userId - * @param {string} blockedReason + * @param userId + * @param blockedReason */ -async function writeBlockedReasonToUser(userId, blockedReason) { +async function writeBlockedReasonToUser(userId: string, blockedReason: string) { try { const { body: { result: setBlockedReasonResult }, @@ -49,9 +49,9 @@ async function writeBlockedReasonToUser(userId, blockedReason) { /** * Convert all reply requests with NORMAL status by the user to BLOCKED status. * - * @param {userId} userId + * @param userId */ -async function processReplyRequests(userId) { +async function processReplyRequests(userId: string) { const NORMAL_REPLY_REQUEST_QUERY = { bool: { must: [{ term: { status: 'NORMAL' } }, { term: { userId } }], @@ -133,9 +133,9 @@ async function processReplyRequests(userId) { /** * Convert all article replies with NORMAL status by the user to BLOCKED status. * - * @param {userId} userId + * @param userId */ -async function processArticleReplies(userId) { +async function processArticleReplies(userId: string) { const NORMAL_ARTICLE_REPLY_QUERY = { nested: { path: 'articleReplies', @@ -198,9 +198,9 @@ async function processArticleReplies(userId) { /** * Convert all article reply feedbacks with NORMAL status by the user to BLOCKED status. * - * @param {userId} userId + * @param userId */ -async function processArticleReplyFeedbacks(userId) { +async function processArticleReplyFeedbacks(userId: string) { const NORMAL_FEEDBACK_QUERY = { bool: { must: [{ term: { status: 'NORMAL' } }, { term: { userId } }], diff --git a/src/util/client.js b/src/util/client.ts similarity index 71% rename from src/util/client.js rename to src/util/client.ts index 065b4a2d..e9621696 100644 --- a/src/util/client.js +++ b/src/util/client.ts @@ -4,10 +4,21 @@ export default new elasticsearch.Client({ node: process.env.ELASTICSEARCH_URL, }); +type ProcessMetaArgs = { + _id: string; + _source: T; + found: boolean; + _score: number; + highlight: object; // FIXME: use ES type + inner_hits: object; // FIXME: use ES type + sort: string; + fields: object; +}; + // Processes {_id, _version, found, _source: {...}} to // {id, ..._source}. // -export function processMeta({ +export function processMeta({ _id: id, _source: source, @@ -21,7 +32,7 @@ export function processMeta({ sort, // cursor when sorted fields, // scripted fields (if any) -}) { +}: ProcessMetaArgs) { if (found || _score !== undefined) { return { id, diff --git a/src/util/getAllDocs.ts b/src/util/getAllDocs.ts index b6453c69..c4d8f905 100644 --- a/src/util/getAllDocs.ts +++ b/src/util/getAllDocs.ts @@ -14,7 +14,7 @@ async function* getAllDocs( index: string, query: object = { match_all: {} }, { scroll = '30s', size = 1000 }: { size?: number; scroll?: string } = {} -): AsyncGenerator { +): AsyncGenerator<{ _source: T }> { let resp = await client.search({ index, scroll, From 123c178f47048020f5d25572370bdb4064cd67de Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 24 Jun 2024 00:35:17 +0800 Subject: [PATCH 02/17] refactor: blockUser script uses typescript defn from rumors-db --- src/scripts/blockUser.ts | 30 ++++++++++++++++++------------ src/util/getAllDocs.ts | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/scripts/blockUser.ts b/src/scripts/blockUser.ts index d169b8dc..4f077452 100644 --- a/src/scripts/blockUser.ts +++ b/src/scripts/blockUser.ts @@ -11,6 +11,10 @@ import getAllDocs from 'util/getAllDocs'; import { updateArticleReplyStatus } from 'graphql/mutations/UpdateArticleReplyStatus'; import { updateArticleReplyByFeedbacks } from 'graphql/mutations/CreateOrUpdateArticleReplyFeedback'; +import type { ReplyRequest } from 'rumors-db/schema/replyrequests'; +import type { Article } from 'rumors-db/schema/articles'; +import type { ArticleReplyFeedback } from 'rumors-db/schema/articlereplyfeedbacks'; + /** * Update user to write blockedReason. Throws if user does not exist. * @@ -38,7 +42,12 @@ async function writeBlockedReasonToUser(userId: string, blockedReason: string) { } } catch (e) { /* istanbul ignore else */ - if (e.message === 'document_missing_exception') { + if ( + e && + typeof e === 'object' && + 'message' in e && + e.message === 'document_missing_exception' + ) { throw new Error(`User with ID=${userId} does not exist`); } @@ -58,11 +67,11 @@ async function processReplyRequests(userId: string) { }, }; - const articleIdsWithNormalReplyRequests = []; + const articleIdsWithNormalReplyRequests: string[] = []; for await (const { _source: { articleId }, - } of getAllDocs('replyrequests', NORMAL_REPLY_REQUEST_QUERY)) { + } of getAllDocs('replyrequests', NORMAL_REPLY_REQUEST_QUERY)) { articleIdsWithNormalReplyRequests.push(articleId); } @@ -158,11 +167,11 @@ async function processArticleReplies(userId: string) { }, }; - const articleRepliesToProcess = []; + const articleRepliesToProcess: Array = []; for await (const { _id, _source: { articleReplies }, - } of getAllDocs('articles', NORMAL_ARTICLE_REPLY_QUERY)) { + } of getAllDocs
('articles', NORMAL_ARTICLE_REPLY_QUERY)) { articleRepliesToProcess.push( ...articleReplies .filter((ar) => { @@ -212,7 +221,7 @@ async function processArticleReplyFeedbacks(userId: string) { for await (const { _source: { articleId, replyId }, - } of getAllDocs('articlereplyfeedbacks', NORMAL_FEEDBACK_QUERY)) { + } of getAllDocs('articlereplyfeedbacks', NORMAL_FEEDBACK_QUERY)) { articleReplyIdsWithNormalFeedbacks.push({ articleId, replyId }); } @@ -276,7 +285,7 @@ async function processArticleReplyFeedbacks(userId: string) { * * @param {string} userId */ -async function processArticles(userId) { +async function processArticles(userId: string) { const NORMAL_ARTICLE_QUERY = { bool: { must: [{ term: { status: 'NORMAL' } }, { term: { userId } }], @@ -300,10 +309,7 @@ async function processArticles(userId) { console.log('Article status update result', updateByQueryResult); } -/** - * @param {object} args - */ -async function main({ userId, blockedReason } = {}) { +async function main({ userId, blockedReason }: { userId: string; blockedReason: string }) { await writeBlockedReasonToUser(userId, blockedReason); await processArticles(userId); await processReplyRequests(userId); @@ -315,7 +321,7 @@ export default main; /* istanbul ignore if */ if (require.main === module) { - const argv = yargs + const argv = await yargs .options({ userId: { alias: 'u', diff --git a/src/util/getAllDocs.ts b/src/util/getAllDocs.ts index c4d8f905..7c3ea449 100644 --- a/src/util/getAllDocs.ts +++ b/src/util/getAllDocs.ts @@ -14,7 +14,7 @@ async function* getAllDocs( index: string, query: object = { match_all: {} }, { scroll = '30s', size = 1000 }: { size?: number; scroll?: string } = {} -): AsyncGenerator<{ _source: T }> { +): AsyncGenerator<{ _id: string; _source: T }> { let resp = await client.search({ index, scroll, From f5f7c29e6a01eec976ee453a28c58779583a298c Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 24 Jun 2024 00:56:41 +0800 Subject: [PATCH 03/17] refactor(CreateOrUpdateArticleReplyFeedback): rewrite into TS --- ... => CreateOrUpdateArticleReplyFeedback.ts} | 63 ++++++++++++------- src/scripts/blockUser.ts | 4 +- 2 files changed, 42 insertions(+), 25 deletions(-) rename src/graphql/mutations/{CreateOrUpdateArticleReplyFeedback.js => CreateOrUpdateArticleReplyFeedback.ts} (78%) diff --git a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts similarity index 78% rename from src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js rename to src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts index 2ac6d65d..68ac34aa 100644 --- a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js +++ b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts @@ -4,6 +4,10 @@ import { assertUser, getContentDefaultStatus } from 'util/user'; import FeedbackVote from 'graphql/models/FeedbackVote'; import ArticleReply from 'graphql/models/ArticleReply'; +import type { Article, ArticleReply as ArticleReplyType } from 'rumors-db/schema/articles'; +import type { ArticleReplyFeedback } from 'rumors-db/schema/articlereplyfeedbacks'; +import type { Reply } from 'rumors-db/schema/replies'; + import client from 'util/client'; export function getArticleReplyFeedbackId({ @@ -11,6 +15,11 @@ export function getArticleReplyFeedbackId({ replyId, userId, appId, +}: { + articleId: string; + replyId: string; + userId: string; + appId: string; }) { return `${articleId}__${replyId}__${userId}__${appId}`; } @@ -19,16 +28,13 @@ export function getArticleReplyFeedbackId({ * Updates the positive and negative feedback count of the article reply with * specified `articleId` and `replyId`. * - * @param {string} articleId - * @param {string} replyId - * @param {object[]} feedbacks - * @returns {object} The updated article reply + * @returns The updated article reply */ export async function updateArticleReplyByFeedbacks( - articleId, - replyId, - feedbacks -) { + articleId: string, + replyId: string, + feedbacks: ArticleReplyFeedback[] +): Promise { const [positiveFeedbackCount, negativeFeedbackCount] = feedbacks .filter(({ status }) => status === 'NORMAL') .reduce( @@ -75,7 +81,7 @@ export async function updateArticleReplyByFeedbacks( }, }, }, - _source: true, + _source: 'true', }); /* istanbul ignore if */ @@ -84,7 +90,7 @@ export async function updateArticleReplyByFeedbacks( } return articleReplyUpdateResult.get._source.articleReplies.find( - (articleReply) => articleReply.replyId === replyId + (articleReply: ArticleReplyType) => articleReply.replyId === replyId ); } @@ -146,22 +152,33 @@ export default { // Fill in reply & article reply author ID // - const [{ userId: replyUserId }, article] = - await loaders.docLoader.loadMany([ - { - index: 'replies', - id: replyId, - }, - { - index: 'articles', - id: articleId, - }, - ]); + const [ + { userId: replyUserId }, + article, + ] = await loaders.docLoader.loadMany([ + { + index: 'replies', + id: replyId, + }, + { + index: 'articles', + id: articleId, + }, + ]) as [Reply, Article]; - const { userId: articleReplyUserId } = article.articleReplies.find( - (ar) => ar.replyId === replyId + const ar = article.articleReplies.find( + ar => ar.replyId === replyId ); + // Make typescript happy + if (!ar) { + throw new Error( + `Cannot find article-reply with article ID = ${articleId} and reply ID = ${replyId}` + ); + } + + const { userId: articleReplyUserId } = ar; + await client.update({ index: 'articlereplyfeedbacks', type: 'doc', diff --git a/src/scripts/blockUser.ts b/src/scripts/blockUser.ts index 4f077452..987b5888 100644 --- a/src/scripts/blockUser.ts +++ b/src/scripts/blockUser.ts @@ -253,8 +253,8 @@ async function processArticleReplyFeedbacks(userId: string) { i, { articleId, replyId }, ] of articleReplyIdsWithNormalFeedbacks.entries()) { - const feedbacks = []; - for await (const { _source: feedback } of getAllDocs( + const feedbacks: ArticleReplyFeedback[] = []; + for await (const { _source: feedback } of getAllDocs( 'articlereplyfeedbacks', { bool: { From 6847000e3288f49f5fc28e715645123ee0f9f04d Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 24 Jun 2024 04:08:49 +0800 Subject: [PATCH 04/17] fix: types --- .../CreateOrUpdateArticleReplyFeedback.ts | 45 +++++++++++-------- src/scripts/blockUser.ts | 17 +++++-- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts index 68ac34aa..efbbaa81 100644 --- a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts +++ b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts @@ -4,7 +4,10 @@ import { assertUser, getContentDefaultStatus } from 'util/user'; import FeedbackVote from 'graphql/models/FeedbackVote'; import ArticleReply from 'graphql/models/ArticleReply'; -import type { Article, ArticleReply as ArticleReplyType } from 'rumors-db/schema/articles'; +import type { + Article, + ArticleReply as ArticleReplyType, +} from 'rumors-db/schema/articles'; import type { ArticleReplyFeedback } from 'rumors-db/schema/articlereplyfeedbacks'; import type { Reply } from 'rumors-db/schema/replies'; @@ -105,7 +108,17 @@ export default { }, async resolve( rootValue, - { articleId, replyId, vote, comment }, + { + articleId, + replyId, + vote, + comment, + }: { + articleId: string; + replyId: string; + vote: 1 | 0 | -1; + comment?: string | null; + }, { user, loaders } ) { assertUser(user); @@ -152,23 +165,19 @@ export default { // Fill in reply & article reply author ID // - const [ - { userId: replyUserId }, - article, - ] = await loaders.docLoader.loadMany([ - { - index: 'replies', - id: replyId, - }, - { - index: 'articles', - id: articleId, - }, - ]) as [Reply, Article]; + const [{ userId: replyUserId }, article] = + (await loaders.docLoader.loadMany([ + { + index: 'replies', + id: replyId, + }, + { + index: 'articles', + id: articleId, + }, + ])) as [Reply, Article]; - const ar = article.articleReplies.find( - ar => ar.replyId === replyId - ); + const ar = article.articleReplies.find((ar) => ar.replyId === replyId); // Make typescript happy if (!ar) { diff --git a/src/scripts/blockUser.ts b/src/scripts/blockUser.ts index 987b5888..f5f793c5 100644 --- a/src/scripts/blockUser.ts +++ b/src/scripts/blockUser.ts @@ -167,7 +167,9 @@ async function processArticleReplies(userId: string) { }, }; - const articleRepliesToProcess: Array = []; + const articleRepliesToProcess: Array< + Article['articleReplies'][0] & { articleId: string } + > = []; for await (const { _id, _source: { articleReplies }, @@ -221,7 +223,10 @@ async function processArticleReplyFeedbacks(userId: string) { for await (const { _source: { articleId, replyId }, - } of getAllDocs('articlereplyfeedbacks', NORMAL_FEEDBACK_QUERY)) { + } of getAllDocs( + 'articlereplyfeedbacks', + NORMAL_FEEDBACK_QUERY + )) { articleReplyIdsWithNormalFeedbacks.push({ articleId, replyId }); } @@ -309,7 +314,13 @@ async function processArticles(userId: string) { console.log('Article status update result', updateByQueryResult); } -async function main({ userId, blockedReason }: { userId: string; blockedReason: string }) { +async function main({ + userId, + blockedReason, +}: { + userId: string; + blockedReason: string; +}) { await writeBlockedReasonToUser(userId, blockedReason); await processArticles(userId); await processReplyRequests(userId); From 087605f1a8cea34a7d38bc845d4431bc82cebda9 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sun, 14 Jul 2024 22:09:25 +0800 Subject: [PATCH 05/17] fix(pacakge.json): add type for cli-progress --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7af2a75d..6ecaba15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-typescript": "^7.24.1", "@google-cloud/storage": "^6.11.0", - "@types/cli-progress": "^3.11.5", + "@types/cli-progress": "^3.11.6", "@types/dotenv": "^8.2.0", "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^5.56.0", diff --git a/package.json b/package.json index 38d80d21..b26bcea0 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@babel/preset-typescript": "^7.24.1", "@google-cloud/storage": "^6.11.0", "@types/node": "^18", - "@types/cli-progress": "^3.11.5", + "@types/cli-progress": "^3.11.6", "@types/dotenv": "^8.2.0", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", From 19c90466a3759dee22b84c087f21aca631d66c5e Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sun, 14 Jul 2024 22:25:43 +0800 Subject: [PATCH 06/17] feat(graphql): type dataloaders - Not really typing every dataloader though, just type the Dataloaders class --- src/graphql/dataLoaders/index.js | 101 ------------------------------- src/graphql/dataLoaders/index.ts | 95 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 101 deletions(-) delete mode 100644 src/graphql/dataLoaders/index.js create mode 100644 src/graphql/dataLoaders/index.ts diff --git a/src/graphql/dataLoaders/index.js b/src/graphql/dataLoaders/index.js deleted file mode 100644 index 4eb82ace..00000000 --- a/src/graphql/dataLoaders/index.js +++ /dev/null @@ -1,101 +0,0 @@ -import docLoaderFactory from './docLoaderFactory'; -import analyticsLoaderFactory from './analyticsLoaderFactory'; -import articleRepliesByReplyIdLoaderFactory from './articleRepliesByReplyIdLoaderFactory'; -import articleCategoriesByCategoryIdLoaderFactory from './articleCategoriesByCategoryIdLoaderFactory'; -import articleReplyFeedbacksLoaderFactory from './articleReplyFeedbacksLoaderFactory'; -import articleCategoryFeedbacksLoaderFactory from './articleCategoryFeedbacksLoaderFactory'; -import searchResultLoaderFactory from './searchResultLoaderFactory'; -import urlLoaderFactory from './urlLoaderFactory'; -import repliedArticleCountLoaderFactory from './repliedArticleCountLoaderFactory'; -import votedArticleReplyCountLoaderFactory from './votedArticleReplyCountLoaderFactory'; -import userLevelLoaderFactory from './userLevelLoaderFactory'; -import userLoaderFactory from './userLoaderFactory'; -import contributionsLoaderFactory from './contributionsLoaderFactory'; - -export default class DataLoaders { - // List of data loaders - // - get docLoader() { - return this._checkOrSetLoader('docLoader', docLoaderFactory); - } - get articleRepliesByReplyIdLoader() { - return this._checkOrSetLoader( - 'articleRepliesByReplyIdLoader', - articleRepliesByReplyIdLoaderFactory - ); - } - get articleCategoriesByCategoryIdLoader() { - return this._checkOrSetLoader( - 'articleCategoriesByCategoryIdLoader', - articleCategoriesByCategoryIdLoaderFactory - ); - } - get articleReplyFeedbacksLoader() { - return this._checkOrSetLoader( - 'articleReplyFeedbacksLoader', - articleReplyFeedbacksLoaderFactory - ); - } - get articleCategoryFeedbacksLoader() { - return this._checkOrSetLoader( - 'articleCategoryFeedbacksLoader', - articleCategoryFeedbacksLoaderFactory - ); - } - get searchResultLoader() { - return this._checkOrSetLoader( - 'searchResultLoader', - searchResultLoaderFactory - ); - } - - get urlLoader() { - return this._checkOrSetLoader('urlLoader', urlLoaderFactory); - } - - get repliedArticleCountLoader() { - return this._checkOrSetLoader( - 'repliedArticleCountLoader', - repliedArticleCountLoaderFactory - ); - } - - get votedArticleReplyCountLoader() { - return this._checkOrSetLoader( - 'votedArticleReplyCountLoader', - votedArticleReplyCountLoaderFactory - ); - } - - get userLoader() { - return this._checkOrSetLoader('userLoader', userLoaderFactory); - } - - get userLevelLoader() { - return this._checkOrSetLoader('userLevelLoader', userLevelLoaderFactory); - } - - get analyticsLoader() { - return this._checkOrSetLoader('analyticsLoader', analyticsLoaderFactory); - } - - get contributionsLoader() { - return this._checkOrSetLoader( - 'contributionsLoader', - contributionsLoaderFactory - ); - } - - // inner-workings - // - constructor() { - this._loaders = {}; - } - - _checkOrSetLoader(name, factoryFn) { - if (this._loaders[name]) return this._loaders[name]; - - this._loaders[name] = factoryFn(this); - return this._loaders[name]; - } -} diff --git a/src/graphql/dataLoaders/index.ts b/src/graphql/dataLoaders/index.ts new file mode 100644 index 00000000..956cc3ef --- /dev/null +++ b/src/graphql/dataLoaders/index.ts @@ -0,0 +1,95 @@ +import docLoaderFactory from './docLoaderFactory'; +import analyticsLoaderFactory from './analyticsLoaderFactory'; +import articleRepliesByReplyIdLoaderFactory from './articleRepliesByReplyIdLoaderFactory'; +import articleCategoriesByCategoryIdLoaderFactory from './articleCategoriesByCategoryIdLoaderFactory'; +import articleReplyFeedbacksLoaderFactory from './articleReplyFeedbacksLoaderFactory'; +import articleCategoryFeedbacksLoaderFactory from './articleCategoryFeedbacksLoaderFactory'; +import searchResultLoaderFactory from './searchResultLoaderFactory'; +import urlLoaderFactory from './urlLoaderFactory'; +import repliedArticleCountLoaderFactory from './repliedArticleCountLoaderFactory'; +import votedArticleReplyCountLoaderFactory from './votedArticleReplyCountLoaderFactory'; +import userLevelLoaderFactory from './userLevelLoaderFactory'; +import userLoaderFactory from './userLoaderFactory'; +import contributionsLoaderFactory from './contributionsLoaderFactory'; + +const LOADER_FACTORY_MAP = { + docLoader: docLoaderFactory, + articleRepliesByReplyIdLoader: articleRepliesByReplyIdLoaderFactory, + articleCategoriesByCategoryIdLoader: + articleCategoriesByCategoryIdLoaderFactory, + articleReplyFeedbacksLoader: articleReplyFeedbacksLoaderFactory, + articleCategoryFeedbacksLoader: articleCategoryFeedbacksLoaderFactory, + searchResultLoader: searchResultLoaderFactory, + urlLoader: urlLoaderFactory, + repliedArticleCountLoader: repliedArticleCountLoaderFactory, + votedArticleReplyCountLoader: votedArticleReplyCountLoaderFactory, + userLoader: userLoaderFactory, + userLevelLoader: userLevelLoaderFactory, + analyticsLoader: analyticsLoaderFactory, + contributionsLoader: contributionsLoaderFactory, +} as const; + +type LoaderFactoryMap = typeof LOADER_FACTORY_MAP; + +export default class DataLoaders { + // List of data loaders + // + get docLoader() { + return this._checkOrSetLoader('docLoader'); + } + get articleRepliesByReplyIdLoader() { + return this._checkOrSetLoader('articleRepliesByReplyIdLoader'); + } + get articleCategoriesByCategoryIdLoader() { + return this._checkOrSetLoader('articleCategoriesByCategoryIdLoader'); + } + get articleReplyFeedbacksLoader() { + return this._checkOrSetLoader('articleReplyFeedbacksLoader'); + } + get articleCategoryFeedbacksLoader() { + return this._checkOrSetLoader('articleCategoryFeedbacksLoader'); + } + get searchResultLoader() { + return this._checkOrSetLoader('searchResultLoader'); + } + get urlLoader() { + return this._checkOrSetLoader('urlLoader'); + } + get repliedArticleCountLoader() { + return this._checkOrSetLoader('repliedArticleCountLoader'); + } + get votedArticleReplyCountLoader() { + return this._checkOrSetLoader('votedArticleReplyCountLoader'); + } + get userLoader() { + return this._checkOrSetLoader('userLoader'); + } + get userLevelLoader() { + return this._checkOrSetLoader('userLevelLoader'); + } + get analyticsLoader() { + return this._checkOrSetLoader('analyticsLoader'); + } + get contributionsLoader() { + return this._checkOrSetLoader('contributionsLoader'); + } + + _loaders: { + -readonly [key in keyof LoaderFactoryMap]?: ReturnType< + LoaderFactoryMap[key] + >; + }; + + // inner-workings + // + constructor() { + this._loaders = {}; + } + + _checkOrSetLoader(name: keyof LoaderFactoryMap) { + if (this._loaders[name]) return this._loaders[name]; + + this._loaders[name] = LOADER_FACTORY_MAP[name](this); + return this._loaders[name]; + } +} From 5fc2c88788433fb3b81a3a8f4b9710a0cbe0ce0c Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 00:08:57 +0800 Subject: [PATCH 07/17] refactor(util): rewrite util/user to Typeascript --- src/util/{user.js => user.ts} | 99 ++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 32 deletions(-) rename src/util/{user.js => user.ts} (67%) diff --git a/src/util/user.js b/src/util/user.ts similarity index 67% rename from src/util/user.js rename to src/util/user.ts index 26b48171..d374bf8f 100644 --- a/src/util/user.js +++ b/src/util/user.ts @@ -16,20 +16,36 @@ import { sample, random } from 'lodash'; import client, { processMeta } from 'util/client'; import rollbar from 'rollbarInstance'; import crypto from 'crypto'; +import { User } from 'rumors-db/schema/users'; + +type UserInContext = User & { + id: string; + /** Filled by createOrUpdateUsear */ + appId: string; +}; + +type UserAppIdPair = { + userId: string; + appId: string; +}; export const AUTH_ERROR_MSG = 'userId is not set via query string.'; /** - * @param {User | {userId: string; appId: string}} param - user in GraphQL context, or {userId, appId} pair + * @param userOrIds - user in GraphQL context, or {userId, appId} pair */ -export function assertUser(userOrIds) { +export function assertUser( + userOrIds: + | UserInContext /* For user instance */ + | UserAppIdPair /* for legacy {userId, appId} pair */ + | null +) { if (userOrIds === null || typeof userOrIds !== 'object') { throw new Error(AUTH_ERROR_MSG); } const userId = - userOrIds.id /* For user instance */ || - userOrIds.userId; /* for legacy {userId, appId} pair */ + 'id' in userOrIds ? userOrIds.id /* For user instance */ : userOrIds.userId; if (!userId) { throw new Error(AUTH_ERROR_MSG); @@ -68,14 +84,18 @@ export const AvatarTypes = { */ export const avatarUrlResolver = (s = 100, d = 'identicon', r = 'g') => - (user) => { + (user: User) => { switch (user.avatarType) { case AvatarTypes.OpenPeeps: return null; case AvatarTypes.Facebook: - return `https://graph.facebook.com/v9.0/${user.facebookId}/picture?height=${s}`; + return 'facebookId' in user + ? `https://graph.facebook.com/v9.0/${user.facebookId}/picture?height=${s}` + : null; case AvatarTypes.Github: - return `https://avatars2.githubusercontent.com/u/${user.githubId}?s=${s}`; + return 'githubId' in user + ? `https://avatars2.githubusercontent.com/u/${user.githubId}?s=${s}` + : null; case AvatarTypes.Gravatar: default: { // return hash based on user email for gravatar url @@ -96,15 +116,15 @@ export const avatarUrlResolver = /** * Returns a list of avatar type options based on information available for a user. */ -export const getAvailableAvatarTypes = (user) => { - let types = [AvatarTypes.OpenPeeps]; +export const getAvailableAvatarTypes = (user: User) => { + const types = [AvatarTypes.OpenPeeps]; if (user?.email) types.push(AvatarTypes.Gravatar); - if (user?.facebookId) types.push(AvatarTypes.Facebook); - if (user?.githubId) types.push(AvatarTypes.Github); + if ('facebookId' in user && user.facebookId) types.push(AvatarTypes.Facebook); + if ('githubId' in user && user.githubId) types.push(AvatarTypes.Github); return types; }; -export const isBackendApp = (appId) => +export const isBackendApp = (appId: UserAppIdPair['appId']) => appId !== 'WEBSITE' && appId !== 'DEVELOPMENT_FRONTEND'; // 6 for appId prefix and 43 for 256bit hashed userId with base64 encoding. @@ -137,7 +157,7 @@ export const generateOpenPeepsAvatar = () => { /** * Given appId, userId pair, where userId could be appUserId or dbUserID, returns the id of corresponding user in db. */ -export const getUserId = ({ appId, userId }) => { +export const getUserId = ({ appId, userId }: UserAppIdPair) => { if (!appId || !isBackendApp(appId) || isDBUserId({ appId, userId })) return userId; else return convertAppUserIdToUserId({ appId, appUserId: userId }); @@ -146,13 +166,13 @@ export const getUserId = ({ appId, userId }) => { /** * Check if the userId for a backend user is the user id in db or it is the app user Id. */ -export const isDBUserId = ({ appId, userId }) => +export const isDBUserId = ({ appId, userId }: UserAppIdPair) => appId && userId && userId.length === BACKEND_USER_ID_LEN && userId.substr(0, 6) === `${encodeAppId(appId)}_`; -export const encodeAppId = (appId) => +export const encodeAppId = (appId: UserAppIdPair['appId']) => crypto .createHash('md5') .update(appId) @@ -160,7 +180,7 @@ export const encodeAppId = (appId) => .replace(/[+/]/g, '') .substr(0, 5); -export const sha256 = (value) => +export const sha256 = (value: string) => crypto .createHash('sha256') .update(value) @@ -170,27 +190,38 @@ export const sha256 = (value) => .replace(/=+$/, ''); /** - * @param {string} appUserId - user ID given by an backend app - * @param {string} appId - app ID - * @returns {string} the id used to index `user` in db + * @param appUserId - user ID given by an backend app + * @param appId - app ID + * @returns the id used to index `user` in db */ -export const convertAppUserIdToUserId = ({ appId, appUserId }) => { +export const convertAppUserIdToUserId = ({ + appId, + appUserId, +}: { + appId: UserAppIdPair['appId']; + appUserId: string; +}) => { return `${encodeAppId(appId)}_${sha256(appUserId)}`; }; /** * Index backend user if not existed, and record the last active time as now. * - * @param {string} userID - either appUserID given by an backend app or userId for frontend users - * @param {string} appId - app ID + * @param userId - either appUserID given by an backend app or userId for frontend users + * @param appId - app ID * - * @returns {user: User, isCreated: boolean} + * @returns { user: User, isCreated: boolean } */ - -export async function createOrUpdateUser({ userId, appId }) { +export async function createOrUpdateUser({ + userId, + appId, +}: UserAppIdPair): Promise<{ + user: UserInContext; + isCreated: boolean; +}> { assertUser({ appId, userId }); const now = new Date().toISOString(); - const dbUserId = exports.getUserId({ appId, userId }); + const dbUserId = getUserId({ appId, userId }); const { body: { result, get: userFound }, } = await client.update({ @@ -216,12 +247,16 @@ export async function createOrUpdateUser({ userId, appId }) { }); const isCreated = result === 'created'; - const user = processMeta({ ...userFound, _id: dbUserId }); + const user = processMeta({ ...userFound, _id: dbUserId }); + + // Make Typescript happy + if (!user) throw new Error('[createOrUpdateUser] Cannot process user'); // checking for collision if ( !isCreated && isBackendApp(appId) && + 'appUserId' in user /* Backend user */ && (user.appId !== appId || user.appUserId !== userId) ) { const errorMessage = `collision found! ${user.appUserId} and ${userId} both hash to ${dbUserId}`; @@ -236,17 +271,17 @@ export async function createOrUpdateUser({ userId, appId }) { } /** - * @param {object} user + * @param user * @returns If the user is blocked (cannot create visible content) */ -export function isUserBlocked(user) { +export function isUserBlocked(user: User) { return !!user.blockedReason; } /** - * @param {object} user - * @returns {'BLOCKED'|'NORMAL'} Default status value for the content generated by the specified user + * @param user + * @returns Default status value for the content generated by the specified user */ -export function getContentDefaultStatus(user) { +export function getContentDefaultStatus(user: User) { return isUserBlocked(user) ? 'BLOCKED' : 'NORMAL'; } From 815d695f9266c137f3ec99527ac614f865069ae1 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 00:24:16 +0800 Subject: [PATCH 08/17] fix(package.json): typing lodash --- package-lock.json | 13 +++++++++++++ package.json | 1 + 2 files changed, 14 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6ecaba15..4a825757 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "@google-cloud/storage": "^6.11.0", "@types/cli-progress": "^3.11.6", "@types/dotenv": "^8.2.0", + "@types/lodash": "^4.17.6", "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", @@ -4694,6 +4695,12 @@ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true + }, "node_modules/@types/long": { "version": "4.0.2", "license": "MIT" @@ -20686,6 +20693,12 @@ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" }, + "@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true + }, "@types/long": { "version": "4.0.2" }, diff --git a/package.json b/package.json index b26bcea0..38fbf757 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@types/node": "^18", "@types/cli-progress": "^3.11.6", "@types/dotenv": "^8.2.0", + "@types/lodash": "^4.17.6", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "apollo-server-testing": "^2.18.2", From 9660cae00036e1f2fd95e0d8aece75453e9385af Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 00:24:58 +0800 Subject: [PATCH 09/17] fix(dataLoaders): make _checkOrSetLoader don't return undefined --- src/graphql/dataLoaders/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/graphql/dataLoaders/index.ts b/src/graphql/dataLoaders/index.ts index 956cc3ef..0f5fad27 100644 --- a/src/graphql/dataLoaders/index.ts +++ b/src/graphql/dataLoaders/index.ts @@ -86,10 +86,13 @@ export default class DataLoaders { this._loaders = {}; } - _checkOrSetLoader(name: keyof LoaderFactoryMap) { - if (this._loaders[name]) return this._loaders[name]; + _checkOrSetLoader( + name: N + ): ReturnType { + const cached = this._loaders[name]; + if (cached) return cached; this._loaders[name] = LOADER_FACTORY_MAP[name](this); - return this._loaders[name]; + return this._loaders[name] as ReturnType; } } From 85c14c00af0c9b6d5343734b461fa79a092d596d Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 00:25:41 +0800 Subject: [PATCH 10/17] refactor(index): extract and type GraphQL context factory function --- src/contextFactory.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ src/index.js | 34 ++++------------------------------ 2 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 src/contextFactory.ts diff --git a/src/contextFactory.ts b/src/contextFactory.ts new file mode 100644 index 00000000..954d8a82 --- /dev/null +++ b/src/contextFactory.ts @@ -0,0 +1,42 @@ +import DataLoaders from './graphql/dataLoaders'; +import { createOrUpdateUser } from './util/user'; + +type ContextFactoryArgs = { + ctx: { + appId: string; + query: { userId?: string }; + state: { user?: { userId?: string } }; + }; +}; + +export default async function contextFactory({ ctx }: ContextFactoryArgs) { + const { + appId, + query: { userId: queryUserId } = {}, + state: { user: { userId: sessionUserId } = {} } = {}, + } = ctx; + + const userId = queryUserId ?? sessionUserId; + + let currentUser = null; + if (appId && userId) { + ({ user: currentUser } = await createOrUpdateUser({ + userId, + appId, + })); + } + + return { + loaders: new DataLoaders(), // new loaders per request + user: currentUser, + + // userId-appId pair + // + userId: currentUser?.id, + appUserId: userId, + appId, + }; +} + +/** GraphQL resolver context */ +export type ResolverContext = Awaited>; diff --git a/src/index.js b/src/index.js index 42a71d73..4d77e475 100644 --- a/src/index.js +++ b/src/index.js @@ -13,11 +13,12 @@ import passport from 'koa-passport'; import { formatError } from 'graphql'; import checkHeaders from './checkHeaders'; import schema from './graphql/schema'; -import DataLoaders from './graphql/dataLoaders'; +import contextFactory from './contextFactory'; + import CookieStore from './CookieStore'; import { loginRouter, authRouter } from './auth'; import rollbar from './rollbarInstance'; -import { AUTH_ERROR_MSG, createOrUpdateUser } from './util/user'; +import { AUTH_ERROR_MSG } from './util/user'; const app = new Koa(); const router = Router(); @@ -106,34 +107,7 @@ export const apolloServer = new ApolloServer({ schema, introspection: true, // Allow introspection in production as well playground: false, - context: async ({ ctx }) => { - const { - appId, - query: { userId: queryUserId } = {}, - state: { user: { userId: sessionUserId } = {} } = {}, - } = ctx; - - const userId = queryUserId ?? sessionUserId; - - let currentUser = null; - if (appId && userId) { - ({ user: currentUser } = await createOrUpdateUser({ - userId, - appId, - })); - } - - return { - loaders: new DataLoaders(), // new loaders per request - user: currentUser, - - // userId-appId pair - // - userId: currentUser?.id, - appUserId: userId, - appId, - }; - }, + context: contextFactory, formatError(err) { // make web clients know they should login // From 2c762cd4e278c078afd99ce112e8da0a0b311791 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 00:26:09 +0800 Subject: [PATCH 11/17] refactor(util): assertUser can narrow down param --- src/util/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/user.ts b/src/util/user.ts index d374bf8f..467d4373 100644 --- a/src/util/user.ts +++ b/src/util/user.ts @@ -39,7 +39,7 @@ export function assertUser( | UserInContext /* For user instance */ | UserAppIdPair /* for legacy {userId, appId} pair */ | null -) { +): asserts userOrIds is UserInContext { if (userOrIds === null || typeof userOrIds !== 'object') { throw new Error(AUTH_ERROR_MSG); } From 88e5061f2d5dd7f3aa0006d3e8d503a448543d9d Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 00:26:38 +0800 Subject: [PATCH 12/17] refactor(CreateOrUpdateArticleReplyFactory): typing rootValue and context --- src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts index efbbaa81..e3542990 100644 --- a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts +++ b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.ts @@ -12,6 +12,7 @@ import type { ArticleReplyFeedback } from 'rumors-db/schema/articlereplyfeedback import type { Reply } from 'rumors-db/schema/replies'; import client from 'util/client'; +import { ResolverContext } from 'contextFactory'; export function getArticleReplyFeedbackId({ articleId, @@ -107,7 +108,7 @@ export default { comment: { type: GraphQLString }, }, async resolve( - rootValue, + rootValue: unknown, { articleId, replyId, @@ -119,7 +120,7 @@ export default { vote: 1 | 0 | -1; comment?: string | null; }, - { user, loaders } + { user, loaders }: ResolverContext ) { assertUser(user); From 209f2ebf6bbc1a7d963b648d1c63022b55f370fd Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 00:28:13 +0800 Subject: [PATCH 13/17] fix(graphql): property type updateArticleReplyStatus --- src/graphql/mutations/UpdateArticleReplyStatus.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphql/mutations/UpdateArticleReplyStatus.js b/src/graphql/mutations/UpdateArticleReplyStatus.js index 3b69640e..8f9428d2 100644 --- a/src/graphql/mutations/UpdateArticleReplyStatus.js +++ b/src/graphql/mutations/UpdateArticleReplyStatus.js @@ -12,7 +12,7 @@ import { getContentDefaultStatus } from 'util/user'; * @param {string} arg.appId * @param {string} arg.status * - * @returns {ArticleReply[]} list of article replies after delete + * @returns {Promise} list of article replies after delete */ export async function updateArticleReplyStatus({ articleId, From 81e06aa76a5fd752ae7aa2100142da997b799eb1 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 01:14:14 +0800 Subject: [PATCH 14/17] fix(util): getAvailableAvatarTypes handles undefined --- src/util/user.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/util/user.ts b/src/util/user.ts index 467d4373..a8235f34 100644 --- a/src/util/user.ts +++ b/src/util/user.ts @@ -116,11 +116,13 @@ export const avatarUrlResolver = /** * Returns a list of avatar type options based on information available for a user. */ -export const getAvailableAvatarTypes = (user: User) => { +export const getAvailableAvatarTypes = (user: User | undefined) => { const types = [AvatarTypes.OpenPeeps]; if (user?.email) types.push(AvatarTypes.Gravatar); - if ('facebookId' in user && user.facebookId) types.push(AvatarTypes.Facebook); - if ('githubId' in user && user.githubId) types.push(AvatarTypes.Github); + if (user && 'facebookId' in user && user.facebookId) + types.push(AvatarTypes.Facebook); + if (user && 'githubId' in user && user.githubId) + types.push(AvatarTypes.Github); return types; }; From 83ab1ac3cb1a2f0ac30d97c73437d9e6397fc728 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 01:25:53 +0800 Subject: [PATCH 15/17] fix(util): fix user test --- src/util/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/user.ts b/src/util/user.ts index a8235f34..52ab269e 100644 --- a/src/util/user.ts +++ b/src/util/user.ts @@ -223,7 +223,7 @@ export async function createOrUpdateUser({ }> { assertUser({ appId, userId }); const now = new Date().toISOString(); - const dbUserId = getUserId({ appId, userId }); + const dbUserId = exports.getUserId({ appId, userId }); // For unit test mocking const { body: { result, get: userFound }, } = await client.update({ From 2113d496e563bd35575ea522b27afca21656553d Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 02:13:03 +0800 Subject: [PATCH 16/17] fix(CreateCategory): move error check in front of accessing data --- src/graphql/mutations/__tests__/CreateCategory.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/graphql/mutations/__tests__/CreateCategory.js b/src/graphql/mutations/__tests__/CreateCategory.js index f41d1597..bd47c3db 100644 --- a/src/graphql/mutations/__tests__/CreateCategory.js +++ b/src/graphql/mutations/__tests__/CreateCategory.js @@ -22,6 +22,8 @@ describe('CreateCategory', () => { { userId: 'test', appId: 'test' } ); + expect(errors).toBeUndefined(); + const categoryId = data.CreateCategory.id; const { body: category } = await client.get({ index: 'categories', @@ -29,7 +31,6 @@ describe('CreateCategory', () => { id: categoryId, }); - expect(errors).toBeUndefined(); expect(category._source).toMatchSnapshot('created category'); // Cleanup From a6562851471eacec2d76e791797ac17564e3ea4f Mon Sep 17 00:00:00 2001 From: MrOrz Date: Mon, 15 Jul 2024 02:14:54 +0800 Subject: [PATCH 17/17] fix(pacakge.json): fix posttest as it now imports typescript files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38fbf757..b185fba0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "seed": "cd src/rumors-db && npm run seed", "pretest": "npm run rumors-db:install && npm run rumors-db:test && mkdir -p build", "test": "NODE_ENV=test ELASTICSEARCH_URL=http://localhost:62223 jest --runInBand", - "posttest": "NODE_ENV=test ELASTICSEARCH_URL=http://localhost:62223 babel-node test/postTest.js", + "posttest": "NODE_ENV=test ELASTICSEARCH_URL=http://localhost:62223 babel-node --extensions .ts,.js test/postTest.js", "start": "pm2-runtime start ecosystem.config.js --env production", "lint": "eslint src/.", "lint:fix": "eslint --fix src/.",