From f1a1cbbc36b9b91e2c834b7b4384c4b90ec1c289 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Mon, 1 Mar 2021 18:53:18 +0800 Subject: [PATCH] include contributions in GetUser query --- .../contributionsLoaderFactory.js | 88 +++++++++++++++++++ .../contirubtionsLoaderFactory.js.snap | 63 +++++++++++++ .../__tests__/contirubtionsLoaderFactory.js | 42 +++++++++ .../dataLoaders/contributionsLoaderFactory.js | 76 ++++++++++++++++ src/graphql/dataLoaders/index.js | 8 ++ src/graphql/models/Contribution.js | 9 ++ src/graphql/models/User.js | 20 +++++ src/graphql/queries/__fixtures__/GetUser.js | 27 ++++++ src/graphql/queries/__tests__/GetUser.js | 16 ++++ .../__tests__/__snapshots__/GetUser.js.snap | 26 +++++- 10 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 src/graphql/dataLoaders/__fixtures__/contributionsLoaderFactory.js create mode 100644 src/graphql/dataLoaders/__tests__/__snapshots__/contirubtionsLoaderFactory.js.snap create mode 100644 src/graphql/dataLoaders/__tests__/contirubtionsLoaderFactory.js create mode 100644 src/graphql/dataLoaders/contributionsLoaderFactory.js create mode 100644 src/graphql/models/Contribution.js diff --git a/src/graphql/dataLoaders/__fixtures__/contributionsLoaderFactory.js b/src/graphql/dataLoaders/__fixtures__/contributionsLoaderFactory.js new file mode 100644 index 00000000..49bb0903 --- /dev/null +++ b/src/graphql/dataLoaders/__fixtures__/contributionsLoaderFactory.js @@ -0,0 +1,88 @@ +export default { + '/replyrequests/doc/replyrequest1': { + userId: 'user1', + createdAt: '2019-01-01T00:00:00.000+08:00', + }, + '/replyrequests/doc/replyrequest2': { + userId: 'user1', + createdAt: '2020-02-01T00:00:00.000+08:00', + }, + '/replyrequests/doc/replyrequest3': { + userId: 'user2', + createdAt: '2020-02-01T00:00:00.000+08:00', + }, + '/replyrequests/doc/replyrequest4': { + userId: 'user1', + createdAt: '2020-02-02T00:00:00.000+08:00', + }, + '/replyrequests/doc/replyrequest5': { + userId: 'user1', + createdAt: '2020-02-03T00:00:00.000+08:00', + }, + '/replyrequests/doc/replyrequest6': { + userId: 'user1', + createdAt: '2020-02-03T00:00:00.000+08:00', + }, + '/replyrequests/doc/replyrequest7': { + userId: 'user1', + createdAt: '2020-02-04T00:00:00.000+08:00', + }, + + '/replies/doc/reply1': { + userId: 'user1', + createdAt: '2019-01-01T00:00:00.000+08:00', + }, + '/replies/doc/reply2': { + userId: 'user2', + createdAt: '2020-01-01T00:00:00.000+08:00', + }, + '/replies/doc/reply3': { + userId: 'user2', + createdAt: '2020-02-03T00:00:00.000+08:00', + }, + '/replies/doc/reply4': { + userId: 'user1', + createdAt: '2020-02-01T00:00:00.000+08:00', + }, + '/replies/doc/reply5': { + userId: 'user1', + createdAt: '2020-02-04T00:00:00.000+08:00', + }, + '/replies/doc/reply6': { + userId: 'user1', + createdAt: '2020-02-09T00:00:00.000+08:00', + }, + '/replies/doc/reply7': { + userId: 'user1', + createdAt: '2020-02-09T00:00:00.000+08:00', + }, + + '/articlereplyfeedbacks/doc/f1': { + userId: 'user1', + createdAt: '2019-01-01T00:00:00.000+08:00', + }, + '/articlereplyfeedbacks/doc/f2': { + userId: 'user2', + createdAt: '2020-01-01T00:00:00.000+08:00', + }, + '/articlereplyfeedbacks/doc/f3': { + userId: 'user2', + createdAt: '2020-02-03T00:00:00.000+08:00', + }, + '/articlereplyfeedbacks/doc/f4': { + userId: 'user1', + createdAt: '2020-02-01T00:00:00.000+08:00', + }, + '/articlereplyfeedbacks/doc/f5': { + userId: 'user1', + createdAt: '2020-02-04T00:00:00.000+08:00', + }, + '/articlereplyfeedbacks/doc/f6': { + userId: 'user1', + createdAt: '2020-02-06T00:00:00.000+08:00', + }, + '/articlereplyfeedbacks/doc/f7': { + userId: 'user1', + createdAt: '2020-02-08T00:00:00.000+08:00', + }, +}; diff --git a/src/graphql/dataLoaders/__tests__/__snapshots__/contirubtionsLoaderFactory.js.snap b/src/graphql/dataLoaders/__tests__/__snapshots__/contirubtionsLoaderFactory.js.snap new file mode 100644 index 00000000..ee897385 --- /dev/null +++ b/src/graphql/dataLoaders/__tests__/__snapshots__/contirubtionsLoaderFactory.js.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`contributionsLoaderFactory should load data for the date range specified 1`] = ` +Array [ + Object { + "count": 1, + "date": "2020-02-02", + }, + Object { + "count": 2, + "date": "2020-02-03", + }, + Object { + "count": 3, + "date": "2020-02-04", + }, + Object { + "count": 1, + "date": "2020-02-06", + }, + Object { + "count": 1, + "date": "2020-02-08", + }, + Object { + "count": 2, + "date": "2020-02-09", + }, +] +`; + +exports[`contributionsLoaderFactory should load last year of data for given userId 1`] = ` +Array [ + Object { + "count": 3, + "date": "2020-02-01", + }, + Object { + "count": 1, + "date": "2020-02-02", + }, + Object { + "count": 2, + "date": "2020-02-03", + }, + Object { + "count": 3, + "date": "2020-02-04", + }, + Object { + "count": 1, + "date": "2020-02-06", + }, + Object { + "count": 1, + "date": "2020-02-08", + }, + Object { + "count": 2, + "date": "2020-02-09", + }, +] +`; diff --git a/src/graphql/dataLoaders/__tests__/contirubtionsLoaderFactory.js b/src/graphql/dataLoaders/__tests__/contirubtionsLoaderFactory.js new file mode 100644 index 00000000..9950caf5 --- /dev/null +++ b/src/graphql/dataLoaders/__tests__/contirubtionsLoaderFactory.js @@ -0,0 +1,42 @@ +import { loadFixtures, unloadFixtures } from 'util/fixtures'; +import DataLoaders from '../index'; +import fixtures from '../__fixtures__/contributionsLoaderFactory'; +import MockDate from 'mockdate'; + +const loader = new DataLoaders(); +MockDate.set(1609430400000); // 2021-01-01T00:00:00.000+08:00 + +describe('contributionsLoaderFactory', () => { + beforeAll(() => loadFixtures(fixtures)); + + it('should load last year of data for given userId', async () => { + const res = await loader.contributionsLoader.load({ + userId: 'user1', + }); + expect(res).toMatchSnapshot(); + }); + + it('should load data for the date range specified', async () => { + expect( + await loader.contributionsLoader.load({ + userId: 'user1', + dateRange: { + gte: '2020-02-01T00:00:00.000Z', + lte: '2020-03-01T00:00:00.000Z', + }, + }) + ).toMatchSnapshot(); + }); + + it('should throw error if userId is not present', async () => { + let error; + try { + await loader.contributionsLoader.load({}); + } catch (e) { + error = e.message; + } + expect(error).toBe('userId is required'); + }); + + afterAll(() => unloadFixtures(fixtures)); +}); diff --git a/src/graphql/dataLoaders/contributionsLoaderFactory.js b/src/graphql/dataLoaders/contributionsLoaderFactory.js new file mode 100644 index 00000000..c935ae08 --- /dev/null +++ b/src/graphql/dataLoaders/contributionsLoaderFactory.js @@ -0,0 +1,76 @@ +import { subDays, startOfWeek } from 'date-fns'; +import DataLoader from 'dataloader'; +import client from 'util/client'; +import { getRangeFieldParamFromArithmeticExpression } from 'graphql/util'; + +const defaultDuration = 365; + +export default () => + new DataLoader( + async statsQueries => { + const body = []; + const defaultEndDate = new Date(); + const defaultStartDate = startOfWeek( + subDays(defaultEndDate, defaultDuration) + ); + const defaultDateRange = { + gt: defaultStartDate, + lte: defaultEndDate, + }; + statsQueries.forEach(({ userId, dateRange = defaultDateRange }) => { + if (!userId) throw new Error('userId is required'); + body.push({ + index: ['replyrequests', 'replies', 'articlereplyfeedbacks'], + type: 'doc', + }); + body.push({ + query: { + bool: { + must: [ + { term: { userId } }, + { + range: { + createdAt: getRangeFieldParamFromArithmeticExpression( + dateRange + ), + }, + }, + ], + }, + }, + size: 0, + aggs: { + contributions: { + date_histogram: { + field: 'createdAt', + interval: 'day', + min_doc_count: 1, + format: 'yyyy-MM-dd', + time_zone: '+08:00', + }, + }, + }, + }); + }); + + return (await client.msearch({ + body, + })).body.responses.map( + ({ + aggregations: { + contributions: { buckets }, + }, + }) => + buckets + ? buckets.map(bucket => ({ + date: bucket.key_as_string, + count: bucket.doc_count, + })) + : [] + ); + }, + { + cacheKeyFn: ({ userId, dateRange }) => + `${userId}/${JSON.stringify(dateRange)}`, + } + ); diff --git a/src/graphql/dataLoaders/index.js b/src/graphql/dataLoaders/index.js index 828bbf2f..4eb82ace 100644 --- a/src/graphql/dataLoaders/index.js +++ b/src/graphql/dataLoaders/index.js @@ -10,6 +10,7 @@ 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 @@ -78,6 +79,13 @@ export default class DataLoaders { return this._checkOrSetLoader('analyticsLoader', analyticsLoaderFactory); } + get contributionsLoader() { + return this._checkOrSetLoader( + 'contributionsLoader', + contributionsLoaderFactory + ); + } + // inner-workings // constructor() { diff --git a/src/graphql/models/Contribution.js b/src/graphql/models/Contribution.js new file mode 100644 index 00000000..beb75978 --- /dev/null +++ b/src/graphql/models/Contribution.js @@ -0,0 +1,9 @@ +import { GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql'; + +export default new GraphQLObjectType({ + name: 'Contribution', + fields: () => ({ + date: { type: GraphQLString }, + count: { type: GraphQLInt }, + }), +}); diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index 3958cd7e..69db7f91 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -12,6 +12,8 @@ import { avatarUrlResolver, } from 'util/user'; import AvatarTypeEnum from './AvatarTypeEnum'; +import Contribution from './Contribution'; +import { timeRangeInput } from 'graphql/util'; /** * Field config helper for current user only field. @@ -129,6 +131,24 @@ const User = new GraphQLObjectType({ createdAt: { type: GraphQLString }, updatedAt: { type: GraphQLString }, lastActiveAt: { type: GraphQLString }, + + contributions: { + type: new GraphQLList(Contribution), + description: 'List of contributions made by the user', + args: { + dateRange: { + type: timeRangeInput, + description: + 'List only the contributions between the specific time range.', + }, + }, + resolve: async ({ id }, { dateRange }, { loaders }) => { + return await loaders.contributionsLoader.load({ + userId: id, + dateRange, + }); + }, + }, }), }); diff --git a/src/graphql/queries/__fixtures__/GetUser.js b/src/graphql/queries/__fixtures__/GetUser.js index a7bd9d09..95d1e8b3 100644 --- a/src/graphql/queries/__fixtures__/GetUser.js +++ b/src/graphql/queries/__fixtures__/GetUser.js @@ -57,4 +57,31 @@ export default { createdAt: '2020-03-06T00:00:00.000Z', updatedAt: '2020-04-06T00:00:00.000Z', }, + + '/replyrequests/doc/replyrequest1': { + userId: 'current-user', + createdAt: '2020-02-01T00:00:00.000+08:00', + }, + '/replyrequests/doc/replyrequest2': { + userId: 'current-user', + createdAt: '2020-02-02T00:00:00.000+08:00', + }, + + '/replies/doc/reply1': { + userId: 'current-user', + createdAt: '2020-02-02T00:00:00.000+08:00', + }, + '/replies/doc/reply2': { + userId: 'current-user', + createdAt: '2020-02-04T00:00:00.000+08:00', + }, + + '/articlereplyfeedbacks/doc/f2': { + userId: 'current-user', + createdAt: '2020-02-03T00:00:00.000+08:00', + }, + '/articlereplyfeedbacks/doc/f3': { + userId: 'current-user', + createdAt: '2020-02-01T00:00:00.000+08:00', + }, }; diff --git a/src/graphql/queries/__tests__/GetUser.js b/src/graphql/queries/__tests__/GetUser.js index ea1d1405..5c606668 100644 --- a/src/graphql/queries/__tests__/GetUser.js +++ b/src/graphql/queries/__tests__/GetUser.js @@ -205,5 +205,21 @@ describe('GetUser', () => { ).toMatchSnapshot('openPeepsUser'); }); + it('returns contributions of a user', async () => { + expect( + await gql` + { + GetUser { + id + contributions(dateRange: { GTE: "2020-02-01", LTE: "2020-02-05" }) { + date + count + } + } + } + `({}, { user: currentUser }) + ).toMatchSnapshot(); + }); + afterAll(() => unloadFixtures(fixtures)); }); diff --git a/src/graphql/queries/__tests__/__snapshots__/GetUser.js.snap b/src/graphql/queries/__tests__/__snapshots__/GetUser.js.snap index 58b8b1d4..778def45 100644 --- a/src/graphql/queries/__tests__/__snapshots__/GetUser.js.snap +++ b/src/graphql/queries/__tests__/__snapshots__/GetUser.js.snap @@ -13,7 +13,7 @@ Object { "total": 3, }, "repliedArticleCount": 2, - "votedArticleReplyCount": 1, + "votedArticleReplyCount": 3, }, }, } @@ -164,3 +164,27 @@ Object { }, } `; + +exports[`GetUser returns contributions of a user 1`] = ` +Object { + "data": Object { + "GetUser": Object { + "contributions": Array [ + Object { + "count": 2, + "date": "2020-02-02", + }, + Object { + "count": 1, + "date": "2020-02-03", + }, + Object { + "count": 1, + "date": "2020-02-04", + }, + ], + "id": "current-user", + }, + }, +} +`;