Skip to content

Commit b34be05

Browse files
committed
feat: allow commenting questions and answers
closes #42
1 parent 1f5f018 commit b34be05

File tree

19 files changed

+869
-193
lines changed

19 files changed

+869
-193
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @param {import('knex').Knex} knex
3+
*/
4+
exports.up = async function up(knex) {
5+
await knex.schema.createTable('question_comments', table => {
6+
table.string('author').notNullable();
7+
table.text('content').notNullable();
8+
table.datetime('created').notNullable();
9+
table.datetime('updated').nullable();
10+
table.string('updatedBy').nullable();
11+
table.integer('questionId').unsigned().notNullable();
12+
table.increments('id');
13+
table
14+
.foreign('questionId')
15+
.references('id')
16+
.inTable('questions')
17+
.onDelete('CASCADE');
18+
});
19+
20+
await knex.schema.createTable('answer_comments', table => {
21+
table.string('author').notNullable();
22+
table.text('content').notNullable();
23+
table.datetime('created').notNullable();
24+
table.datetime('updated').nullable();
25+
table.string('updatedBy').nullable();
26+
table.integer('answerId').unsigned().notNullable();
27+
table.increments('id');
28+
table
29+
.foreign('answerId')
30+
.references('id')
31+
.inTable('answers')
32+
.onDelete('CASCADE');
33+
});
34+
};
35+
36+
/**
37+
* @param {import('knex').Knex} knex
38+
*/
39+
exports.down = async function down(knex) {
40+
await knex.schema.dropTable('question_comments');
41+
await knex.schema.dropTable('answer_comments');
42+
};

plugins/qeta-backend/src/database/DatabaseQetaStore.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ describe.each(databases.eachSupportedId())(
9595
await knex('answer_votes').del();
9696
await knex('question_views').del();
9797
await knex('tags').del();
98+
await knex('question_comments').del();
99+
await knex('answer_comments').del();
98100
});
99101

100102
describe('questions and answers database', () => {
@@ -131,6 +133,13 @@ describe.each(databases.eachSupportedId())(
131133
expect(ans).toBeDefined();
132134
expect(ans?.content).toEqual('answer');
133135
expect(ans?.questionId).toEqual(id);
136+
137+
const ans2 = await storage.commentAnswer(
138+
ans?.id ?? 0,
139+
'user:default/user',
140+
'this is comment',
141+
);
142+
expect(ans2?.comments?.length).toEqual(1);
134143
});
135144

136145
it('should fetch list of questions', async () => {
@@ -266,6 +275,13 @@ describe.each(databases.eachSupportedId())(
266275
expect(ret2?.entities?.sort()).toEqual(
267276
['component:default/comp2', 'component:default/comp3'].sort(),
268277
);
278+
279+
const ret3 = await storage.commentQuestion(
280+
id1.id,
281+
'user:default/user',
282+
'this is comment',
283+
);
284+
expect(ret3?.comments?.length).toEqual(1);
269285
});
270286

271287
it('should update question', async () => {

plugins/qeta-backend/src/database/DatabaseQetaStore.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ export type RawTagEntity = {
6767
tag: string;
6868
};
6969

70+
export type RawCommentEntity = {
71+
id: number;
72+
author: string;
73+
content: string;
74+
created: Date;
75+
updated: Date;
76+
updatedBy: string;
77+
};
78+
7079
export class DatabaseQetaStore implements QetaStore {
7180
private constructor(private readonly db: Knex) {}
7281

@@ -236,6 +245,7 @@ export class DatabaseQetaStore implements QetaStore {
236245
true,
237246
true,
238247
true,
248+
true,
239249
);
240250
}
241251

@@ -288,6 +298,65 @@ export class DatabaseQetaStore implements QetaStore {
288298
return this.mapQuestion(questions[0], false, false, true);
289299
}
290300

301+
async commentQuestion(
302+
question_id: number,
303+
user_ref: string,
304+
content: string,
305+
): Promise<MaybeQuestion> {
306+
await this.db
307+
.insert({
308+
author: user_ref,
309+
content,
310+
created: new Date(),
311+
questionId: question_id,
312+
})
313+
.into('question_comments');
314+
315+
return await this.getQuestion(user_ref, question_id, false);
316+
}
317+
318+
async deleteQuestionComment(
319+
question_id: number,
320+
id: number,
321+
user_ref: string,
322+
): Promise<MaybeQuestion> {
323+
await this.db('question_comments')
324+
.where('id', '=', id)
325+
.where('author', '=', user_ref)
326+
.where('questionId', '=', question_id)
327+
.delete();
328+
return this.getQuestion(user_ref, question_id, false);
329+
}
330+
331+
async commentAnswer(
332+
answer_id: number,
333+
user_ref: string,
334+
content: string,
335+
): Promise<MaybeAnswer> {
336+
await this.db
337+
.insert({
338+
author: user_ref,
339+
content,
340+
created: new Date(),
341+
answerId: answer_id,
342+
})
343+
.into('answer_comments');
344+
return this.getAnswer(answer_id);
345+
}
346+
347+
async deleteAnswerComment(
348+
answer_id: number,
349+
id: number,
350+
user_ref: string,
351+
): Promise<MaybeAnswer> {
352+
await this.db('answer_comments')
353+
.where('id', '=', id)
354+
.where('author', '=', user_ref)
355+
.where('answerId', '=', answer_id)
356+
.delete();
357+
return this.getAnswer(answer_id);
358+
}
359+
291360
async updateQuestion(
292361
id: number,
293362
user_ref: string,
@@ -358,7 +427,7 @@ export class DatabaseQetaStore implements QetaStore {
358427

359428
async getAnswer(answerId: number): Promise<MaybeAnswer> {
360429
const answers = await this.getAnswerBaseQuery().where('id', '=', answerId);
361-
return this.mapAnswer(answers[0], true);
430+
return this.mapAnswer(answers[0], true, true);
362431
}
363432

364433
async deleteAnswer(user_ref: string, id: number): Promise<boolean> {
@@ -489,13 +558,17 @@ export class DatabaseQetaStore implements QetaStore {
489558
addAnswers?: boolean,
490559
addVotes?: boolean,
491560
addEntities?: boolean,
561+
addComments?: boolean,
492562
): Promise<Question> {
493563
// TODO: This could maybe done with join
494564
const additionalInfo = await Promise.all([
495565
this.getQuestionTags(val.id),
496-
addAnswers ? this.getQuestionAnswers(val.id, addVotes) : undefined,
566+
addAnswers
567+
? this.getQuestionAnswers(val.id, addVotes, addComments)
568+
: undefined,
497569
addVotes ? this.getQuestionVotes(val.id) : undefined,
498570
addEntities ? this.getQuestionEntities(val.id) : undefined,
571+
addComments ? this.getQuestionComments(val.id) : undefined,
499572
]);
500573
return {
501574
id: val.id,
@@ -515,14 +588,19 @@ export class DatabaseQetaStore implements QetaStore {
515588
votes: additionalInfo[2],
516589
entities: additionalInfo[3],
517590
trend: this.mapToInteger(val.trend),
591+
comments: additionalInfo[4],
518592
};
519593
}
520594

521595
private async mapAnswer(
522596
val: RawAnswerEntity,
523597
addVotes?: boolean,
598+
addComments?: boolean,
524599
): Promise<Answer> {
525-
const votes = addVotes ? await this.getAnswerVotes(val.id) : undefined;
600+
const additionalInfo = await Promise.all([
601+
addVotes ? this.getAnswerVotes(val.id) : undefined,
602+
addComments ? this.getAnswerComments(val.id) : undefined,
603+
]);
526604
return {
527605
id: val.id,
528606
questionId: val.questionId,
@@ -533,7 +611,8 @@ export class DatabaseQetaStore implements QetaStore {
533611
updated: val.updated,
534612
updatedBy: val.updatedBy,
535613
score: this.mapToInteger(val.score),
536-
votes,
614+
votes: additionalInfo[0],
615+
comments: additionalInfo[1],
537616
};
538617
}
539618

@@ -553,6 +632,22 @@ export class DatabaseQetaStore implements QetaStore {
553632
return rows.map(val => val.tag);
554633
}
555634

635+
private async getQuestionComments(
636+
questionId: number,
637+
): Promise<RawCommentEntity[]> {
638+
return this.db<RawCommentEntity>('question_comments') // nosonar
639+
.where('question_comments.questionId', '=', questionId)
640+
.select();
641+
}
642+
643+
private async getAnswerComments(
644+
answerId: number,
645+
): Promise<RawCommentEntity[]> {
646+
return this.db<RawCommentEntity>('answer_comments') // nosonar
647+
.where('answer_comments.answerId', '=', answerId)
648+
.select();
649+
}
650+
556651
private async getQuestionEntities(questionId: number): Promise<string[]> {
557652
const rows = await this.db<RawTagEntity>('entities') // nosonar
558653
.leftJoin(
@@ -596,14 +691,15 @@ export class DatabaseQetaStore implements QetaStore {
596691
private async getQuestionAnswers(
597692
questionId: number,
598693
addVotes?: boolean,
694+
addComments?: boolean,
599695
): Promise<Answer[]> {
600696
const rows = await this.getAnswerBaseQuery()
601697
.where('questionId', '=', questionId)
602698
.orderBy('answers.correct', 'desc')
603699
.orderBy('answers.created');
604700
return await Promise.all(
605701
rows.map(async val => {
606-
return this.mapAnswer(val, addVotes);
702+
return this.mapAnswer(val, addVotes, addComments);
607703
}),
608704
);
609705
}

plugins/qeta-backend/src/database/QetaStore.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface Question {
1818
own?: boolean;
1919
votes?: Vote[];
2020
trend?: number;
21+
comments?: Comment[];
2122
}
2223

2324
export interface Answer {
@@ -33,6 +34,7 @@ export interface Answer {
3334
ownVote?: number;
3435
own?: boolean;
3536
votes?: Vote[];
37+
comments?: Comment[];
3638
}
3739

3840
export interface Vote {
@@ -41,6 +43,15 @@ export interface Vote {
4143
timestamp: Date;
4244
}
4345

46+
export interface Comment {
47+
author: string;
48+
content: string;
49+
created: Date;
50+
own?: boolean;
51+
updated?: Date;
52+
updatedBy?: string;
53+
}
54+
4455
export type MaybeAnswer = Answer | null;
4556
export type MaybeQuestion = Question | null;
4657

@@ -133,6 +144,30 @@ export interface QetaStore {
133144
components?: string[],
134145
): Promise<Question>;
135146

147+
/**
148+
* Comment question
149+
* @param question_id question id
150+
* @param user_ref user
151+
* @param content comment content
152+
*/
153+
commentQuestion(
154+
question_id: number,
155+
user_ref: string,
156+
content: string,
157+
): Promise<MaybeQuestion>;
158+
159+
/**
160+
* Delete question comment
161+
* @param question_id question id
162+
* @param id comment id
163+
* @param user_ref username
164+
*/
165+
deleteQuestionComment(
166+
question_id: number,
167+
id: number,
168+
user_ref: string,
169+
): Promise<MaybeQuestion>;
170+
136171
/**
137172
* Update question
138173
* @param id question id
@@ -170,6 +205,30 @@ export interface QetaStore {
170205
answer: string,
171206
): Promise<MaybeAnswer>;
172207

208+
/**
209+
* Comment answer
210+
* @param answer_id answer id
211+
* @param user_ref user commenting
212+
* @param content comment content
213+
*/
214+
commentAnswer(
215+
answer_id: number,
216+
user_ref: string,
217+
content: string,
218+
): Promise<MaybeAnswer>;
219+
220+
/**
221+
* Delete answer comment
222+
* @param answer_id answer id
223+
* @param id comment id
224+
* @param user_ref username
225+
*/
226+
deleteAnswerComment(
227+
answer_id: number,
228+
id: number,
229+
user_ref: string,
230+
): Promise<MaybeAnswer>;
231+
173232
/**
174233
* Update answer to a question
175234
* @param user_ref user name of the user updating the answer

plugins/qeta-backend/src/service/router.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ describe('createRouter', () => {
5757
let app: express.Express;
5858

5959
const qetaStore: jest.Mocked<QetaStore> = {
60+
commentAnswer: jest.fn(),
61+
commentQuestion: jest.fn(),
62+
deleteAnswerComment: jest.fn(),
63+
deleteQuestionComment: jest.fn(),
6064
getQuestions: jest.fn(),
6165
getQuestion: jest.fn(),
6266
getQuestionByAnswerId: jest.fn(),

0 commit comments

Comments
 (0)