Skip to content

Commit f5c2372

Browse files
authored
Template based reports backend updates (#398)
1 parent 4bb3f21 commit f5c2372

16 files changed

Lines changed: 223 additions & 36 deletions

backend/src/api/report/reportCreate.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Error403 from '../../errors/Error403'
12
import Permissions from '../../security/permissions'
23
import track from '../../segment/track'
34
import ReportService from '../../services/reportService'
@@ -6,6 +7,15 @@ import PermissionChecker from '../../services/user/permissionChecker'
67
export default async (req, res) => {
78
new PermissionChecker(req).validateHas(Permissions.values.reportCreate)
89

10+
if (req.body.isTemplate) {
11+
await req.responseHandler.error(
12+
req,
13+
res,
14+
new Error403(req.language, 'errors.report.templateReportsCreateNotAllowed'),
15+
)
16+
return
17+
}
18+
919
const payload = await new ReportService(req).create(req.body)
1020

1121
track(

backend/src/api/report/reportUpdate.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
import Error403 from '../../errors/Error403'
12
import Permissions from '../../security/permissions'
23
import track from '../../segment/track'
34
import ReportService from '../../services/reportService'
45
import PermissionChecker from '../../services/user/permissionChecker'
56

67
export default async (req, res) => {
78
new PermissionChecker(req).validateHas(Permissions.values.reportEdit)
9+
if (req.body.isTemplate) {
10+
await req.responseHandler.error(
11+
req,
12+
res,
13+
new Error403(req.language, 'errors.report.templateReportsUpdateNotAllowed'),
14+
)
15+
return
16+
}
817

918
const payload = await new ReportService(req).update(req.params.id, req.body)
1019

backend/src/cubejs/schema/Members.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ cube(`Members`, {
1313
GROUP BY m.id`,
1414

1515
preAggregations: {
16+
MembersCumulative: {
17+
measures: [Members.cumulativeCount],
18+
dimensions: [
19+
Members.score,
20+
Members.location,
21+
Members.tenantId,
22+
Members.isTeamMember,
23+
Members.isBot,
24+
],
25+
timeDimension: Members.joinedAt,
26+
granularity: `day`,
27+
refreshKey: {
28+
every: `10 minute`,
29+
},
30+
},
31+
1632
ActiveMembers: {
1733
measures: [Members.count],
1834
dimensions: [
@@ -95,6 +111,13 @@ cube(`Members`, {
95111
sql: `time_to_first_interaction`,
96112
shown: false,
97113
},
114+
115+
cumulativeCount: {
116+
type: `count`,
117+
rollingWindow: {
118+
trailing: `unbounded`,
119+
},
120+
},
98121
},
99122

100123
dimensions: {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import TenantService from '../../../services/tenantService'
2+
import getUserContext from '../../utils/getUserContext'
3+
import ReportService from '../../../services/reportService'
4+
5+
export default async () => {
6+
const tenants = await TenantService._findAndCountAllForEveryUser({})
7+
8+
// for each tenant
9+
for (const tenant of tenants.rows) {
10+
const userContext = await getUserContext(tenant.id)
11+
const rs = new ReportService(userContext)
12+
13+
console.log(`Creating members report for tenant ${tenant.id}`)
14+
await rs.create({
15+
name: 'Members report',
16+
public: false,
17+
isTemplate: true,
18+
})
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
ALTER TABLE public.reports DROP COLUMN "isTemplate";
2+
3+
drop materialized view "memberActivityAggregatesMVs";
4+
5+
create materialized view "memberActivityAggregatesMVs" as
6+
SELECT m.id,
7+
max(a."timestamp") AS "lastActive",
8+
count(a.id) AS "activityCount",
9+
array_agg(DISTINCT a.platform) FILTER (WHERE a.platform IS NOT NULL) AS "activeOn",
10+
round(avg(
11+
CASE
12+
WHEN (a.sentiment ->> 'sentiment'::text) IS NOT NULL
13+
THEN (a.sentiment ->> 'sentiment'::text)::double precision
14+
ELSE NULL::double precision
15+
END)::numeric, 2) AS "averageSentiment"
16+
FROM members m
17+
LEFT JOIN activities a ON m.id = a."memberId" AND a."deletedAt" IS NULL
18+
GROUP BY m.id;
19+
20+
create unique index ix_memberactivityaggregatesmvs_memberid
21+
on "memberActivityAggregatesMVs" (id);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
ALTER TABLE public."reports" ADD COLUMN "isTemplate" BOOLEAN NOT NULL DEFAULT FALSE;
2+
3+
drop materialized view "memberActivityAggregatesMVs";
4+
5+
create materialized view "memberActivityAggregatesMVs" as
6+
SELECT m.id,
7+
max(a."timestamp") AS "lastActive",
8+
count(a.id) AS "activityCount",
9+
array_agg(
10+
distinct (concat(a.platform,':',a.type))
11+
) filter (
12+
where
13+
a.platform is not null
14+
) AS "activityTypes",
15+
array_agg(DISTINCT a.platform) FILTER (WHERE a.platform IS NOT NULL) AS "activeOn",
16+
count(distinct a.timestamp::date) AS "activeDaysCount",
17+
round(avg(
18+
CASE
19+
WHEN (a.sentiment ->> 'sentiment'::text) IS NOT NULL
20+
THEN (a.sentiment ->> 'sentiment'::text)::double precision
21+
ELSE NULL::double precision
22+
END)::numeric, 2) AS "averageSentiment"
23+
FROM members m
24+
LEFT JOIN activities a ON m.id = a."memberId" AND a."deletedAt" IS NULL
25+
GROUP BY m.id;
26+
27+
create unique index ix_memberactivityaggregatesmvs_memberid
28+
on "memberActivityAggregatesMVs" (id);

backend/src/database/models/memberActivityAggregatesMV.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export default (sequelize) => {
2121
activityCount: {
2222
type: DataTypes.INTEGER,
2323
},
24+
activeDaysCount: {
25+
type: DataTypes.INTEGER,
26+
},
2427
})
2528

2629
return memberActivityAggregatesMV

backend/src/database/models/report.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export default (sequelize) => {
1414
allowNull: false,
1515
defaultValue: false,
1616
},
17+
isTemplate: {
18+
type: DataTypes.BOOLEAN,
19+
allowNull: false,
20+
defaultValue: false,
21+
},
1722
name: {
1823
type: DataTypes.TEXT,
1924
allowNull: false,

backend/src/database/repositories/__tests__/conversationRepository.test.ts

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -440,32 +440,26 @@ describe('ConversationRepository tests', () => {
440440
'tags',
441441
'tasks',
442442
'toMerge',
443+
'activeOn',
444+
'identities',
445+
'activeDaysCount',
446+
'activityTypes',
443447
])
444448

445449
const conversation1Expected = {
446450
...conversation1Created,
447451
conversationStarter: {
448452
...SequelizeTestUtils.objectWithoutKey(activity1Created, ['parent', 'tasks']),
449-
member: SequelizeTestUtils.objectWithoutKey(memberReturnedWithinConversations, [
450-
'activeOn',
451-
'identities',
452-
'activityTypes',
453-
]),
453+
member: memberReturnedWithinConversations,
454454
},
455455
lastReplies: [
456456
{
457457
...SequelizeTestUtils.objectWithoutKey(activity2Created, ['parent', 'tasks']),
458-
member: SequelizeTestUtils.objectWithoutKey(memberReturnedWithinConversations, [
459-
'activeOn',
460-
'identities',
461-
]),
458+
member: memberReturnedWithinConversations,
462459
},
463460
{
464461
...SequelizeTestUtils.objectWithoutKey(activity3Created, ['parent', 'tasks']),
465-
member: SequelizeTestUtils.objectWithoutKey(memberReturnedWithinConversations, [
466-
'activeOn',
467-
'identities',
468-
]),
462+
member: memberReturnedWithinConversations,
469463
},
470464
],
471465
}
@@ -474,19 +468,12 @@ describe('ConversationRepository tests', () => {
474468
...conversation2Created,
475469
conversationStarter: {
476470
...SequelizeTestUtils.objectWithoutKey(activity4Created, ['parent', 'tasks']),
477-
member: SequelizeTestUtils.objectWithoutKey(memberReturnedWithinConversations, [
478-
'activeOn',
479-
'identities',
480-
'activityTypes',
481-
]),
471+
member: memberReturnedWithinConversations,
482472
},
483473
lastReplies: [
484474
{
485475
...SequelizeTestUtils.objectWithoutKey(activity5Created, ['parent', 'tasks']),
486-
member: SequelizeTestUtils.objectWithoutKey(memberReturnedWithinConversations, [
487-
'activeOn',
488-
'identities',
489-
]),
476+
member: memberReturnedWithinConversations,
490477
},
491478
],
492479
}
@@ -495,19 +482,12 @@ describe('ConversationRepository tests', () => {
495482
...conversation3Created,
496483
conversationStarter: {
497484
...SequelizeTestUtils.objectWithoutKey(activity6Created, ['parent', 'tasks']),
498-
member: SequelizeTestUtils.objectWithoutKey(memberReturnedWithinConversations, [
499-
'activeOn',
500-
'identities',
501-
'activityTypes',
502-
]),
485+
member: memberReturnedWithinConversations,
503486
},
504487
lastReplies: [
505488
{
506489
...SequelizeTestUtils.objectWithoutKey(activity7Created, ['parent', 'tasks']),
507-
member: SequelizeTestUtils.objectWithoutKey(memberReturnedWithinConversations, [
508-
'activeOn',
509-
'identities',
510-
]),
490+
member: memberReturnedWithinConversations,
511491
},
512492
],
513493
}

backend/src/database/repositories/__tests__/memberRepository.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ describe('MemberRepository tests', () => {
8787
noMerge: [],
8888
toMerge: [],
8989
activityCount: 0,
90+
activeDaysCount: 0,
9091
lastActive: null,
9192
averageSentiment: 0,
9293
lastActivity: null,
@@ -195,6 +196,7 @@ describe('MemberRepository tests', () => {
195196
noMerge: [],
196197
toMerge: [],
197198
activityCount: 0,
199+
activeDaysCount: 0,
198200
averageSentiment: 0,
199201
lastActive: null,
200202
lastActivity: null,
@@ -333,6 +335,7 @@ describe('MemberRepository tests', () => {
333335
noMerge: [],
334336
toMerge: [],
335337
activityCount: 0,
338+
activeDaysCount: 0,
336339
averageSentiment: 0,
337340
lastActive: null,
338341
lastActivity: null,
@@ -511,6 +514,7 @@ describe('MemberRepository tests', () => {
511514
delete member1Returned.activeOn
512515
delete member1Returned.identities
513516
delete member1Returned.activityTypes
517+
delete member1Returned.activeDaysCount
514518

515519
const found = await MemberRepository.findOne(
516520
{ email: 'joan@crowd.dev' },
@@ -604,6 +608,7 @@ describe('MemberRepository tests', () => {
604608
delete member1Returned.activeOn
605609
delete member1Returned.identities
606610
delete member1Returned.activityTypes
611+
delete member1Returned.activeDaysCount
607612

608613
const found = await MemberRepository.memberExists(
609614
'test1',
@@ -1312,6 +1317,7 @@ describe('MemberRepository tests', () => {
13121317
noMerge: [],
13131318
toMerge: [],
13141319
activityCount: 0,
1320+
activeDaysCount: 0,
13151321
averageSentiment: 0,
13161322
lastActive: null,
13171323
lastActivity: null,
@@ -1459,6 +1465,7 @@ describe('MemberRepository tests', () => {
14591465
noMerge: [],
14601466
toMerge: [],
14611467
activityCount: 0,
1468+
activeDaysCount: 0,
14621469
averageSentiment: 0,
14631470
lastActive: null,
14641471
lastActivity: null,
@@ -1554,6 +1561,7 @@ describe('MemberRepository tests', () => {
15541561
notes: [],
15551562
tasks: [],
15561563
activityCount: 0,
1564+
activeDaysCount: 0,
15571565
averageSentiment: 0,
15581566
lastActive: null,
15591567
lastActivity: null,

0 commit comments

Comments
 (0)