diff --git a/src/graphql/__tests__/util.js b/src/graphql/__tests__/util.js index 2d912765..110e61c5 100644 --- a/src/graphql/__tests__/util.js +++ b/src/graphql/__tests__/util.js @@ -120,6 +120,7 @@ if (process.env.GCS_BUCKET_NAME) { createdAt, // eslint-disable-next-line no-unused-vars updatedAt, + text, ...aiResponse } = await createTranscript( { @@ -130,50 +131,21 @@ if (process.env.GCS_BUCKET_NAME) { { id: 'user-id', appId: 'app-id' } ); + // Expect some keywords are identified. + // The whole text are not always 100% identical, but these keywords should be always included. + expect(text).toMatch(/^排/); + expect(text).toMatch(/德國醫學博士艾倫斯特發現/); + expect(text).toMatch(/汗也具有調節體溫的重要作用/); expect(aiResponse).toMatchInlineSnapshot(` - Object { - "appId": "app-id", - "docId": "foo", - "status": "SUCCESS", - "text": "排 - 汗 - 汗和排尿的差別 - 想要健康長壽就要想辦法一天一次大量排汗 - 德國醫學博士艾倫斯特發現:所有運動選手中 - 唯獨馬拉松選手沒有罹患癌症病例。 - 艾倫斯特博士採集了每天跑步 30 公里以上的 - 馬拉松選手的汗水,分析其汗水的成份結果 - 發現汗水中含有 鎘 鉛銅鎳等之重金屬物質。 - 證明出汗是排泄體內疲勞物質及對人體有害的 - 重金屬毒素的重要途徑 - 雖然排泄體內不需要物質的基本功能,有排便 - 排尿與出汗。而尿也會排出重金屬,但是排出 - 功能卻遠不及汗。 - 汗與尿中的重金屬元素量 - 鉛(微克)鎘(微克)鈷(微克) - 6.5 - 1.2 - 0.65 - 0.6 - 汗 84 - 尿 4.9 - 100 克 中〉 - 鎳(微克)銅(毫克) - 32 - 0.11 - 3.1 - 0.01 - 汗也具有調節體溫的重要作用。 全身健康的 - 出汗,就能夠強化現代最欠缺的體溫調節功能 - 與自律神經。 - 藉著汗,氣化熱消耗熱量,能夠提升代謝力, - 不但減少體脂肪,還有助於消除肥胖。 - 可以先從關掉冷氣做起 - ", - "type": "TRANSCRIPT", - "userId": "user-id", - } - `); + Object { + "appId": "app-id", + "docId": "foo", + "status": "SUCCESS", + "type": "TRANSCRIPT", + "userId": "user-id", + } + `); + // Cleanup await client.delete({ index: 'airesponses', diff --git a/src/graphql/models/Article.js b/src/graphql/models/Article.js index 8164a7f7..e0b4d921 100644 --- a/src/graphql/models/Article.js +++ b/src/graphql/models/Article.js @@ -44,6 +44,7 @@ import Hyperlink from './Hyperlink'; import ReplyRequest from './ReplyRequest'; import ArticleTypeEnum from './ArticleTypeEnum'; import Cooccurrence from './Cooccurrence'; +import Contributor from './Contributor'; const ATTACHMENT_URL_DURATION_DAY = 1; @@ -580,6 +581,28 @@ const Article = new GraphQLObjectType({ }, }), }, + contributors: { + type: new GraphQLNonNull( + new GraphQLList(new GraphQLNonNull(Contributor)) + ), + description: 'Transcript contributors of the article', + resolve: ({ contributors }) => contributors ?? [], + }, + transcribedAt: { + type: GraphQLString, + description: 'Time when the article was last transcribed', + resolve: async ({ contributors }) => { + if (!contributors || contributors.length === 0) { + return null; + } + const maxUpdatedAt = new Date( + Math.max( + ...contributors.map(contributor => new Date(contributor.updatedAt)) + ) + ); + return maxUpdatedAt.toISOString(); + }, + }, }), }); diff --git a/src/graphql/models/Contributor.js b/src/graphql/models/Contributor.js new file mode 100644 index 00000000..5bf4cb78 --- /dev/null +++ b/src/graphql/models/Contributor.js @@ -0,0 +1,16 @@ +import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql'; +import User, { userFieldResolver } from './User'; + +export default new GraphQLObjectType({ + name: 'Contributor', + fields: () => ({ + user: { + type: User, + description: 'The user who contributed to this article.', + resolve: userFieldResolver, + }, + userId: { type: GraphQLNonNull(GraphQLString) }, + appId: { type: GraphQLNonNull(GraphQLString) }, + updatedAt: { type: GraphQLString }, + }), +}); diff --git a/src/graphql/mutations/CreateArticle.js b/src/graphql/mutations/CreateArticle.js index 0aace2d6..31ea74be 100644 --- a/src/graphql/mutations/CreateArticle.js +++ b/src/graphql/mutations/CreateArticle.js @@ -82,6 +82,7 @@ async function createNewArticle({ text, reference: originalReference, user }) { attachmentUrl: '', attachmentHash: '', status: getContentDefaultStatus(user), + contributors: [], }, }, refresh: 'true', // Make sure the data is indexed when we create ReplyRequest diff --git a/src/graphql/mutations/CreateMediaArticle.js b/src/graphql/mutations/CreateMediaArticle.js index f3454356..f6fb1da7 100644 --- a/src/graphql/mutations/CreateMediaArticle.js +++ b/src/graphql/mutations/CreateMediaArticle.js @@ -162,6 +162,7 @@ async function createNewMediaArticle({ articleType, attachmentHash, status: getContentDefaultStatus(user), + contributors: [], }, }); diff --git a/src/graphql/mutations/__tests__/CreateMediaArticle.js b/src/graphql/mutations/__tests__/CreateMediaArticle.js index 2031ede9..7c4ffd94 100644 --- a/src/graphql/mutations/__tests__/CreateMediaArticle.js +++ b/src/graphql/mutations/__tests__/CreateMediaArticle.js @@ -84,6 +84,7 @@ describe('creation', () => { "articleReplies": Array [], "articleType": "IMAGE", "attachmentHash": "mock_image_hash", + "contributors": Array [], "createdAt": "2017-01-28T08:45:57.011Z", "hyperlinks": Array [], "lastRequestedAt": "2017-01-28T08:45:57.011Z", diff --git a/src/graphql/mutations/__tests__/__snapshots__/CreateArticle.js.snap b/src/graphql/mutations/__tests__/__snapshots__/CreateArticle.js.snap index cbf68fa2..bbcad3c4 100644 --- a/src/graphql/mutations/__tests__/__snapshots__/CreateArticle.js.snap +++ b/src/graphql/mutations/__tests__/__snapshots__/CreateArticle.js.snap @@ -28,6 +28,7 @@ Object { "articleType": "TEXT", "attachmentHash": "", "attachmentUrl": "", + "contributors": Array [], "createdAt": "2017-01-28T08:45:57.011Z", "hyperlinks": Array [ Object { diff --git a/src/graphql/queries/ListArticles.js b/src/graphql/queries/ListArticles.js index a05cad39..6ce629c5 100644 --- a/src/graphql/queries/ListArticles.js +++ b/src/graphql/queries/ListArticles.js @@ -16,6 +16,7 @@ import { intRangeInput, timeRangeInput, moreLikeThisInput, + userAndExistInput, getRangeFieldParamFromArithmeticExpression, createCommonListFilter, attachCommonListFilter, @@ -136,22 +137,12 @@ export default { articleRepliesFrom: { description: 'Show only articles with(out) article replies created by specified user', - type: new GraphQLInputObjectType({ - name: 'UserAndExistInput', - fields: { - userId: { - type: new GraphQLNonNull(GraphQLString), - }, - exists: { - type: GraphQLBoolean, - defaultValue: true, - description: ` - When true (or not specified), return only entries with the specified user's involvement. - When false, return only entries that the specified user did not involve. - `, - }, - }, - }), + type: userAndExistInput, + }, + transcribedBy: { + description: + 'Show only articles with(out) article transcript contributed by specified user', + type: userAndExistInput, }, hasArticleReplyWithMorePositiveFeedback: { type: GraphQLBoolean, @@ -548,6 +539,22 @@ export default { }); } + if (filter.transcribedBy) { + (filter.transcribedBy.exists === false + ? mustNotQueries + : filterQueries + ).push({ + nested: { + path: 'contributors', + query: { + term: { + 'contributors.userId': filter.transcribedBy.userId, + }, + }, + }, + }); + } + if (filter.replyTypes) { filterQueries.push({ nested: { diff --git a/src/graphql/queries/__fixtures__/ListArticles.js b/src/graphql/queries/__fixtures__/ListArticles.js index 2cc002ea..f4ff8713 100644 --- a/src/graphql/queries/__fixtures__/ListArticles.js +++ b/src/graphql/queries/__fixtures__/ListArticles.js @@ -46,6 +46,28 @@ export default { negativeFeedbackCount: 0, }, ], + contributors: [ + { + userId: 'user1', + appId: 'WEBSITE', + updatedAt: '2020-02-05T14:41:19.044Z', + }, + { + userId: 'user2', + appId: 'WEBSITE', + updatedAt: '2020-02-09T14:41:19.044Z', + }, + { + userId: 'user3', + appId: 'WEBSITE', + updatedAt: '2020-02-08T14:41:19.044Z', + }, + { + userId: 'user4', + appId: 'WEBSITE', + updatedAt: '2020-02-07T14:41:19.044Z', + }, + ], attachmentUrl: '', attachmentHash: '', articleType: 'TEXT', @@ -105,6 +127,13 @@ export default { negativeFeedbackCount: 0, }, ], + contributors: [ + { + userId: 'user1', + appId: 'WEBSITE', + updatedAt: '2020-02-04T15:11:04.472Z', + }, + ], attachmentUrl: '', attachmentHash: '', articleType: 'TEXT', diff --git a/src/graphql/queries/__tests__/ListArticles.js b/src/graphql/queries/__tests__/ListArticles.js index d7aa4a4e..44e10e4c 100644 --- a/src/graphql/queries/__tests__/ListArticles.js +++ b/src/graphql/queries/__tests__/ListArticles.js @@ -885,6 +885,50 @@ describe('ListArticles', () => { ).toMatchSnapshot('do not have articleReply from user1'); }); + it('filters via transcribedBy', async () => { + expect( + await gql` + { + ListArticles(filter: { transcribedBy: { userId: "user1" } }) { + edges { + node { + id + contributors { + user { + id + } + } + transcribedAt + } + } + } + } + `({}, { appId: 'WEBSITE' }) + ).toMatchSnapshot('transcribedBy user1'); + + expect( + await gql` + { + ListArticles( + filter: { transcribedBy: { userId: "user1", exists: false } } + ) { + edges { + node { + id + contributors { + user { + id + } + } + transcribedAt + } + } + } + } + `({}, { appId: 'WEBSITE' }) + ).toMatchSnapshot('is not transcribedBy user1'); + }); + it('filters by reply types', async () => { expect( await gql` diff --git a/src/graphql/queries/__tests__/__snapshots__/ListArticles.js.snap b/src/graphql/queries/__tests__/__snapshots__/ListArticles.js.snap index 6b3b5e42..d4812ef7 100644 --- a/src/graphql/queries/__tests__/__snapshots__/ListArticles.js.snap +++ b/src/graphql/queries/__tests__/__snapshots__/ListArticles.js.snap @@ -709,6 +709,104 @@ Object { } `; +exports[`ListArticles filters via transcribedBy: is not transcribedBy user1 1`] = ` +Object { + "data": Object { + "ListArticles": Object { + "edges": Array [ + Object { + "node": Object { + "contributors": Array [], + "id": "listArticleTest7", + "transcribedAt": null, + }, + }, + Object { + "node": Object { + "contributors": Array [], + "id": "listArticleTest6", + "transcribedAt": null, + }, + }, + Object { + "node": Object { + "contributors": Array [], + "id": "listArticleTest5", + "transcribedAt": null, + }, + }, + Object { + "node": Object { + "contributors": Array [], + "id": "listArticleTest4", + "transcribedAt": null, + }, + }, + Object { + "node": Object { + "contributors": Array [], + "id": "listArticleTest3", + "transcribedAt": null, + }, + }, + ], + }, + }, +} +`; + +exports[`ListArticles filters via transcribedBy: transcribedBy user1 1`] = ` +Object { + "data": Object { + "ListArticles": Object { + "edges": Array [ + Object { + "node": Object { + "contributors": Array [ + Object { + "user": Object { + "id": "user1", + }, + }, + ], + "id": "listArticleTest2", + "transcribedAt": "2020-02-04T15:11:04.472Z", + }, + }, + Object { + "node": Object { + "contributors": Array [ + Object { + "user": Object { + "id": "user1", + }, + }, + Object { + "user": Object { + "id": "user2", + }, + }, + Object { + "user": Object { + "id": "user3", + }, + }, + Object { + "user": Object { + "id": "user4", + }, + }, + ], + "id": "listArticleTest1", + "transcribedAt": "2020-02-09T14:41:19.044Z", + }, + }, + ], + }, + }, +} +`; + exports[`ListArticles filters via articleRepliesFrom: do not have articleReply from user1 1`] = ` Object { "data": Object { diff --git a/src/graphql/util.js b/src/graphql/util.js index f2ef4141..322e4962 100644 --- a/src/graphql/util.js +++ b/src/graphql/util.js @@ -103,6 +103,23 @@ export const moreLikeThisInput = new GraphQLInputObjectType({ }, }); +export const userAndExistInput = new GraphQLInputObjectType({ + name: 'UserAndExistInput', + fields: { + userId: { + type: new GraphQLNonNull(GraphQLString), + }, + exists: { + type: GraphQLBoolean, + defaultValue: true, + description: ` + When true (or not specified), return only entries with the specified user's involvement. + When false, return only entries that the specified user did not involve. + `, + }, + }, +}); + export function createFilterType(typeName, args) { const filterType = new GraphQLInputObjectType({ name: typeName, diff --git a/test/rumors-db b/test/rumors-db index 032b83e6..623c0c38 160000 --- a/test/rumors-db +++ b/test/rumors-db @@ -1 +1 @@ -Subproject commit 032b83e6a9b5758bc979dde99db81333501755f3 +Subproject commit 623c0c38741b900d69a09c89a7908d2abea47bc4