Initiatives: add queries to mirror to implement DiscourseQueries (#1490)

We defined a DiscourseQueries interface, intended as a subset of
the Discourse plugin's MirrorRepository methods. This subset is
used by the Initiatives plugin to source Iniaitive data.

We're now adding the new methods it needed to the MirrorRepository.
This commit is contained in:
Robin van Boven 2020-01-11 11:49:45 +01:00 committed by GitHub
parent adec1b0b5d
commit 5cf0fba634
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 179 additions and 0 deletions

View File

@ -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() {

View File

@ -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<Post>;
}
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<Post> {
return this._db
.prepare(
@ -304,6 +347,35 @@ export class SqliteMirrorRepository
}));
}
postsInTopic(topicId: TopicId, numberOfPosts: number): $ReadOnlyArray<Post> {
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<string> {
return this._db
.prepare("SELECT username FROM users")

View File

@ -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: "<p>Valid post</p>",
};
const p2: Post = {
// Deliberately scramble id, order of `indexWithinTopic` should matter.
id: 121,
topicId: 123,
indexWithinTopic: 1,
replyToPostIndex: null,
timestampMs: 456888,
authorUsername: "credbot",
cooked: "<p>Follow up 1</p>",
};
const p3: Post = {
id: 102,
topicId: 123,
indexWithinTopic: 2,
replyToPostIndex: null,
timestampMs: 456999,
authorUsername: "credbot",
cooked: "<p>Follow up 2</p>",
};
// 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]);
});
});

View File

@ -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