diff --git a/src/plugins/discourse/createGraph.test.js b/src/plugins/discourse/createGraph.test.js index 8e6052d..00f8ef3 100644 --- a/src/plugins/discourse/createGraph.test.js +++ b/src/plugins/discourse/createGraph.test.js @@ -83,6 +83,12 @@ describe("plugins/discourse/createGraph", () => { findUsername() { throw new Error("Method findUsername should be unused by createGraph"); } + topicById() { + throw new Error("Method topicById should be unused by createGraph"); + } + postsInTopic() { + throw new Error("Method postsInTopic should be unused by createGraph"); + } } function example() { diff --git a/src/plugins/discourse/mirrorRepository.js b/src/plugins/discourse/mirrorRepository.js index 63c1ad8..9dd3d2b 100644 --- a/src/plugins/discourse/mirrorRepository.js +++ b/src/plugins/discourse/mirrorRepository.js @@ -67,6 +67,19 @@ export interface ReadRepository { * Note: input username is case-insensitive. */ findUsername(username: string): ?string; + + /** + * Gets a Topic by ID. + */ + topicById(id: TopicId): ?Topic; + + /** + * Gets a number of Posts in a given Topic. Starting with the first post, + * ordered by `indexWithinTopic`. + * + * numberOfPosts: the maximum number of posts to get (may return fewer). + */ + postsInTopic(topicId: TopicId, numberOfPosts: number): $ReadOnlyArray; } export type SyncHeads = {| @@ -278,6 +291,36 @@ export class SqliteMirrorRepository })); } + topicById(id: TopicId): ?Topic { + const res = this._db + .prepare( + dedent`\ + SELECT + id, + category_id, + title, + timestamp_ms, + bumped_ms, + author_username + FROM topics + WHERE id = :id` + ) + .get({id}); + + if (!res) { + return null; + } + + return { + id: res.id, + categoryId: res.category_id, + title: res.title, + timestampMs: res.timestamp_ms, + bumpedMs: res.bumped_ms, + authorUsername: res.author_username, + }; + } + posts(): $ReadOnlyArray { return this._db .prepare( @@ -304,6 +347,35 @@ export class SqliteMirrorRepository })); } + postsInTopic(topicId: TopicId, numberOfPosts: number): $ReadOnlyArray { + return this._db + .prepare( + dedent`\ + SELECT + id, + timestamp_ms, + author_username, + topic_id, + index_within_topic, + reply_to_post_index, + cooked + FROM posts + WHERE topic_id = :topic_id + ORDER BY index_within_topic ASC + LIMIT :max` + ) + .all({topic_id: topicId, max: numberOfPosts}) + .map((x) => ({ + id: x.id, + timestampMs: x.timestamp_ms, + authorUsername: x.author_username, + topicId: x.topic_id, + indexWithinTopic: x.index_within_topic, + replyToPostIndex: x.reply_to_post_index, + cooked: x.cooked, + })); + } + users(): $ReadOnlyArray { return this._db .prepare("SELECT username FROM users") diff --git a/src/plugins/discourse/mirrorRepository.test.js b/src/plugins/discourse/mirrorRepository.test.js index 89a4d52..99af8a0 100644 --- a/src/plugins/discourse/mirrorRepository.test.js +++ b/src/plugins/discourse/mirrorRepository.test.js @@ -302,4 +302,96 @@ describe("plugins/discourse/mirrorRepository", () => { if (error) throw error; }).toThrow("FOREIGN KEY constraint failed"); }); + + it("topicById gets the matching topics", () => { + // Given + const db = new Database(":memory:"); + const url = "http://example.com"; + const repository = new SqliteMirrorRepository(db, url); + const topic1: Topic = { + id: 123, + categoryId: 42, + title: "Sample topic 1", + timestampMs: 456789, + bumpedMs: 456999, + authorUsername: "credbot", + }; + const topic2: Topic = { + id: 456, + categoryId: 42, + title: "Sample topic 2", + timestampMs: 456789, + bumpedMs: 456999, + authorUsername: "credbot", + }; + + // When + repository.addTopic(topic1); + repository.addTopic(topic2); + const actualT1 = repository.topicById(topic1.id); + const actualT2 = repository.topicById(topic2.id); + + // Then + expect(actualT1).toEqual(topic1); + expect(actualT2).toEqual(topic2); + }); + + it("postsInTopic gets the number of posts requested", () => { + // Given + const db = new Database(":memory:"); + const url = "http://example.com"; + const repository = new SqliteMirrorRepository(db, url); + const topic: Topic = { + id: 123, + categoryId: 1, + title: "Sample topic", + timestampMs: 456789, + bumpedMs: 456999, + authorUsername: "credbot", + }; + const p1: Post = { + id: 100, + topicId: 123, + indexWithinTopic: 0, + replyToPostIndex: null, + timestampMs: 456789, + authorUsername: "credbot", + cooked: "

Valid post

", + }; + const p2: Post = { + // Deliberately scramble id, order of `indexWithinTopic` should matter. + id: 121, + topicId: 123, + indexWithinTopic: 1, + replyToPostIndex: null, + timestampMs: 456888, + authorUsername: "credbot", + cooked: "

Follow up 1

", + }; + const p3: Post = { + id: 102, + topicId: 123, + indexWithinTopic: 2, + replyToPostIndex: null, + timestampMs: 456999, + authorUsername: "credbot", + cooked: "

Follow up 2

", + }; + + // When + repository.addTopic(topic); + // Deliberately scramble the adding order, order of `indexWithinTopic` should matter. + repository.addPost(p3); + repository.addPost(p1); + repository.addPost(p2); + const posts0 = repository.postsInTopic(topic.id, 0); + const posts2 = repository.postsInTopic(topic.id, 2); + const posts5 = repository.postsInTopic(topic.id, 5); + + // Then + // Note: these are in order, starting from the opening post. + expect(posts0).toEqual([]); + expect(posts2).toEqual([p1, p2]); + expect(posts5).toEqual([p1, p2, p3]); + }); }); diff --git a/src/plugins/initiatives/discourse.test.js b/src/plugins/initiatives/discourse.test.js index 0ea8a44..5313861 100644 --- a/src/plugins/initiatives/discourse.test.js +++ b/src/plugins/initiatives/discourse.test.js @@ -7,6 +7,7 @@ import { type DiscourseQueries, } from "./discourse"; import {type Initiative} from "./initiative"; +import type {ReadRepository} from "../discourse/mirrorRepository"; import type {Topic, Post, CategoryId, TopicId} from "../discourse/fetch"; import {NodeAddress} from "../../core/graph"; import dedent from "../../util/dedent"; @@ -156,6 +157,14 @@ describe("plugins/initiatives/discourse", () => { } }); + describe("DiscourseQueries", () => { + it("should be a subset of the ReadRepository interface", () => { + const _unused_toDiscourseQueries = ( + x: ReadRepository + ): DiscourseQueries => x; + }); + }); + describe("DiscourseInitiativeRepository", () => { it("uses topicsInCategories to find initiative topics", () => { // Given