initial commit

This commit is contained in:
Wyatt Johnson
2018-06-16 17:20:51 -06:00
commit 02e1236792
39 changed files with 5258 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
import { Db } from 'mongodb';
import { FilterQuery } from './types';
import { defaults } from 'lodash';
import uuid from 'uuid';
import { Omit } from 'talk-common/types';
import dotize from 'dotize';
export interface Asset {
readonly id: string;
url: string;
scraped?: Date;
closedAt?: Date;
closedMessage?: string;
title?: string;
description?: string;
image?: string;
section?: string;
subsection?: string;
author?: string;
publication_date?: Date;
modified_date?: Date;
created_at: Date;
}
export type CreateAssetInput = Pick<Asset, 'id' | 'url'>;
export async function create(db: Db, input: CreateAssetInput): Promise<Asset> {
const now = new Date();
// Construct the filter.
const filter: FilterQuery<Asset> = {};
if (input.id) {
filter.id = input.id;
} else {
filter.url = input.url;
}
// Craft the update object.
const update: { $setOnInsert: Asset } = {
$setOnInsert: defaults(input, {
id: uuid.v4(),
created_at: now,
}),
};
// Perform the upsert operation.
const result = await db
.collection<Asset>('assets')
.findOneAndUpdate(filter, update, {
// Create the object if it doesn't already exist.
upsert: true,
// False to return the updated document instead of the original
// document.
returnOriginal: false,
});
return result.value;
}
export async function exists(db: Db, id: string): Promise<boolean> {
// TODO: implement
// const cursor = await db.collection<Asset>('assets').find({ id }).limit(1);
return null;
}
export async function retrieve(db: Db, id: string): Promise<Asset> {
return await db.collection<Asset>('assets').findOne({ id });
}
export async function retrieveMany(
db: Db,
ids: string[]
): Promise<Array<Asset>> {
const cursor = await db
.collection<Asset>('assets')
.find({ id: { $in: ids } });
const assets = await cursor.toArray();
return ids.map(id => assets.find(asset => asset.id === id));
}
export type UpdateAssetInput = Omit<
Partial<Asset>,
'id' | 'url' | 'created_at'
>;
export async function update(
db: Db,
id: string,
update: UpdateAssetInput
): Promise<Readonly<Asset>> {
const result = await db.collection<Asset>('assets').findOneAndUpdate(
{ id },
// Only update fields that have been updated.
{ $set: dotize(update) },
// False to return the updated document instead of the original
// document.
{ returnOriginal: false }
);
return result.value;
}
+99
View File
@@ -0,0 +1,99 @@
import { Db } from 'mongodb';
import { Omit, Sub } from 'talk-common/types';
import { merge } from 'lodash';
import uuid from 'uuid';
export interface BodyHistoryItem {
body: string;
created_at: Date;
}
export interface StatusHistoryItem {
status: CommentStatus; // TODO: migrate field
assigned_by?: string;
created_at: Date;
}
export enum CommentStatus {
ACCEPTED = 'ACCEPTED',
REJECTED = 'REJECTED',
PREMOD = 'PREMOD',
SYSTEM_WITHHELD = 'SYSTEM_WITHHELD',
NONE = 'NONE',
}
export interface ActionCounts {
[_: string]: number;
}
export interface Comment {
readonly id: string;
parent_id?: string;
author_id: string;
asset_id: string;
body: string;
body_history: BodyHistoryItem[];
status: CommentStatus;
status_history: StatusHistoryItem[];
action_counts: ActionCounts;
reply_count: number;
created_at: Date;
deleted_at?: Date;
}
export type CreateCommentInput = Omit<
Comment,
'id' | 'created_at' | 'reply_count' | 'body_history' | 'status_history'
>;
export async function create(
db: Db,
input: CreateCommentInput
): Promise<Readonly<Comment>> {
const now = new Date();
// Pull out some useful properties from the input.
const { body, status } = input;
// default are the properties set by the application when a new comment is
// created.
const defaults: Sub<Comment, CreateCommentInput> = {
id: uuid.v4(),
created_at: now,
reply_count: 0,
body_history: [
{
body,
created_at: now,
},
],
status_history: [
{
status,
created_at: now,
},
],
};
// Merge the defaults and the input together.
const comment: Comment = merge({}, defaults, input);
// TODO: Check for existence of the parent ID before we create the comment.
// TODO: Check for existence of the asset ID before we create the comment.
// Insert it into the database.
await db.collection<Comment>('comments').insertOne(comment);
// TODO: update reply count of parent if exists.
return comment;
}
async function incrementReplyCount(db: Db, parentID: string): Promise<void> {
return null;
}
export async function retrieve(db: Db, id: string): Promise<Comment> {
return null;
}
+121
View File
@@ -0,0 +1,121 @@
import { Db } from 'mongodb';
import { defaultsDeep } from 'lodash';
import dotize from 'dotize';
// selector is the single document selector for the Settings model stored in the
// settings collection in MongoDB.
const selector = { id: '1' };
export interface Wordlist {
banned: string[];
suspect: string[];
}
export enum Moderation {
PRE = 'PRE',
POST = 'POST',
}
export interface Settings {
readonly id: string;
moderation: Moderation;
requireEmailConfirmation: boolean;
infoBoxEnable: boolean;
infoBoxContent?: string;
questionBoxEnable: boolean;
questionBoxIcon?: string;
questionBoxContent?: string;
premodLinksEnable: boolean;
autoCloseStream: boolean;
closedTimeout: number;
closedMessage?: string;
customCssUrl?: string;
disableCommenting: boolean;
disableCommentingMessage?: string;
// editCommentWindowLength is the length of time (in milliseconds) after a
// comment is posted that it can still be edited by the author.
editCommentWindowLength: number;
charCountEnable: boolean;
charCount?: number;
organizationName?: string;
organizationContactEmail?: string;
// wordlist stores all the banned/suspect words.
wordlist: Wordlist;
// domains is the set of whitelisted domains.
domains: string[];
}
const defaultSettings: Settings = {
// Include the selector.
...selector,
// Default to post moderation.
moderation: Moderation.POST,
// Email confirmation is default off.
requireEmailConfirmation: false,
infoBoxEnable: false,
questionBoxEnable: false,
premodLinksEnable: false,
autoCloseStream: false,
// Two weeks timeout.
closedTimeout: 60 * 60 * 24 * 7 * 2,
disableCommenting: false,
editCommentWindowLength: 30 * 1000,
charCountEnable: false,
wordlist: {
suspect: [],
banned: [],
},
domains: [],
};
export async function create(
db: Db,
settingsInput: Partial<Settings>
): Promise<Readonly<Settings>> {
const result = await db
.collection<Settings>('settings')
.findOneAndReplace(
selector,
defaultsDeep({}, settingsInput, defaultSettings),
{
upsert: true,
returnOriginal: false,
}
);
return result.value;
}
export async function retrieve(db: Db): Promise<Readonly<Settings>> {
const settings = await db
.collection<Settings>('settings')
.findOne(selector);
if (!settings) {
throw new Error('settings not initialized'); // FIXME: return actual typed error
}
return settings;
}
export async function update(
db: Db,
update: Partial<Settings>
): Promise<Readonly<Settings>> {
// Get the settings from the database.
const result = await db.collection<Settings>('settings').findOneAndUpdate(
selector,
// Only update fields that have been updated.
{ $set: dotize(update) },
// False to return the updated document instead of the original
// document.
{ returnOriginal: false }
);
return result.value;
}
+5
View File
@@ -0,0 +1,5 @@
import { FilterQuery } from 'mongodb';
import { Writeable } from '../../common/types';
export type FilterQuery<T> = Writeable<Partial<T>> &
FilterQuery<Writeable<Partial<T>>>;