mirror of
https://github.com/wassname/talk.git
synced 2026-07-08 23:27:55 +08:00
added tenant + graph support
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export interface ActionCounts {
|
||||
[_: string]: number;
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Db } from 'mongodb';
|
||||
import { FilterQuery } from './types';
|
||||
import { Db, Collection } from 'mongodb';
|
||||
import Query, { FilterQuery } from './query';
|
||||
import { defaults } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { Omit } from 'talk-common/types';
|
||||
import dotize from 'dotize';
|
||||
import { TenantResource } from 'talk-server/models/tenant';
|
||||
|
||||
export interface Asset {
|
||||
function collection(db: Db): Collection<Asset> {
|
||||
return db.collection<Asset>('assets');
|
||||
}
|
||||
|
||||
export interface Asset extends TenantResource {
|
||||
readonly id: string;
|
||||
url: string;
|
||||
scraped?: Date;
|
||||
@@ -24,21 +29,28 @@ export interface Asset {
|
||||
|
||||
export type CreateAssetInput = Pick<Asset, 'id' | 'url'>;
|
||||
|
||||
export async function create(db: Db, input: CreateAssetInput): Promise<Asset> {
|
||||
export async function create(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
input: CreateAssetInput
|
||||
): Promise<Asset> {
|
||||
const now = new Date();
|
||||
|
||||
// Construct the filter.
|
||||
const filter: FilterQuery<Asset> = {};
|
||||
const query = new Query<Asset>(collection(db)).where({
|
||||
tenant_id: tenantID,
|
||||
});
|
||||
if (input.id) {
|
||||
filter.id = input.id;
|
||||
query.where({ id: input.id });
|
||||
} else {
|
||||
filter.url = input.url;
|
||||
query.where({ url: input.url });
|
||||
}
|
||||
|
||||
// Craft the update object.
|
||||
const update: { $setOnInsert: Asset } = {
|
||||
$setOnInsert: defaults(input, {
|
||||
id: uuid.v4(),
|
||||
tenant_id: tenantID,
|
||||
created_at: now,
|
||||
}),
|
||||
};
|
||||
@@ -46,7 +58,7 @@ export async function create(db: Db, input: CreateAssetInput): Promise<Asset> {
|
||||
// Perform the upsert operation.
|
||||
const result = await db
|
||||
.collection<Asset>('assets')
|
||||
.findOneAndUpdate(filter, update, {
|
||||
.findOneAndUpdate(query.filter, update, {
|
||||
// Create the object if it doesn't already exist.
|
||||
upsert: true,
|
||||
// False to return the updated document instead of the original
|
||||
@@ -57,24 +69,24 @@ export async function create(db: Db, input: CreateAssetInput): Promise<Asset> {
|
||||
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 retrieve(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
id: string
|
||||
): Promise<Asset> {
|
||||
return await db
|
||||
.collection<Asset>('assets')
|
||||
.findOne({ id, tenant_id: tenantID });
|
||||
}
|
||||
|
||||
export async function retrieveMany(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
ids: string[]
|
||||
): Promise<Array<Asset>> {
|
||||
const cursor = await db
|
||||
.collection<Asset>('assets')
|
||||
.find({ id: { $in: ids } });
|
||||
.find({ id: { $in: ids }, tenant_id: tenantID });
|
||||
|
||||
const assets = await cursor.toArray();
|
||||
|
||||
@@ -83,16 +95,17 @@ export async function retrieveMany(
|
||||
|
||||
export type UpdateAssetInput = Omit<
|
||||
Partial<Asset>,
|
||||
'id' | 'url' | 'created_at'
|
||||
'id' | 'tenant_id' | 'url' | 'created_at'
|
||||
>;
|
||||
|
||||
export async function update(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
update: UpdateAssetInput
|
||||
): Promise<Readonly<Asset>> {
|
||||
const result = await db.collection<Asset>('assets').findOneAndUpdate(
|
||||
{ id },
|
||||
{ id, tenant_id: tenantID },
|
||||
// Only update fields that have been updated.
|
||||
{ $set: dotize(update) },
|
||||
// False to return the updated document instead of the original
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Db } from 'mongodb';
|
||||
import { Db, Collection } from 'mongodb';
|
||||
import { Omit, Sub } from 'talk-common/types';
|
||||
import { merge } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { Connection, Edge, Cursor } from 'talk-server/models/connection';
|
||||
import Query from 'talk-server/models/query';
|
||||
import { ActionCounts } from 'talk-server/models/actions';
|
||||
import { TenantResource } from 'talk-server/models/tenant';
|
||||
|
||||
function collection(db: Db): Collection<Comment> {
|
||||
return db.collection<Comment>('comments');
|
||||
}
|
||||
|
||||
export interface BodyHistoryItem {
|
||||
body: string;
|
||||
@@ -22,11 +30,7 @@ export enum CommentStatus {
|
||||
NONE = 'NONE',
|
||||
}
|
||||
|
||||
export interface ActionCounts {
|
||||
[_: string]: number;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
export interface Comment extends TenantResource {
|
||||
readonly id: string;
|
||||
parent_id?: string;
|
||||
author_id: string;
|
||||
@@ -39,15 +43,24 @@ export interface Comment {
|
||||
reply_count: number;
|
||||
created_at: Date;
|
||||
deleted_at?: Date;
|
||||
metadata?: {
|
||||
[_: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export type CreateCommentInput = Omit<
|
||||
Comment,
|
||||
'id' | 'created_at' | 'reply_count' | 'body_history' | 'status_history'
|
||||
| 'id'
|
||||
| 'tenant_id'
|
||||
| 'created_at'
|
||||
| 'reply_count'
|
||||
| 'body_history'
|
||||
| 'status_history'
|
||||
>;
|
||||
|
||||
export async function create(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
input: CreateCommentInput
|
||||
): Promise<Readonly<Comment>> {
|
||||
const now = new Date();
|
||||
@@ -59,6 +72,7 @@ export async function create(
|
||||
// created.
|
||||
const defaults: Sub<Comment, CreateCommentInput> = {
|
||||
id: uuid.v4(),
|
||||
tenant_id: tenantID,
|
||||
created_at: now,
|
||||
reply_count: 0,
|
||||
body_history: [
|
||||
@@ -83,17 +97,200 @@ export async function create(
|
||||
// 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);
|
||||
await collection(db).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,
|
||||
tenantID: string,
|
||||
id: string
|
||||
): Promise<Readonly<Comment>> {
|
||||
return collection(db).findOne({ id, tenant_id: tenantID });
|
||||
}
|
||||
|
||||
export async function retrieve(db: Db, id: string): Promise<Comment> {
|
||||
return null;
|
||||
export async function retrieveMany(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
ids: string[]
|
||||
): Promise<Readonly<Comment>[]> {
|
||||
const cursor = await collection(db).find({
|
||||
id: {
|
||||
$in: ids,
|
||||
},
|
||||
tenant_id: tenantID,
|
||||
});
|
||||
|
||||
const comments = await cursor.toArray();
|
||||
|
||||
return ids.map(id => comments.find(comment => comment.id === id));
|
||||
}
|
||||
|
||||
export enum CommentSort {
|
||||
CREATED_AT_DESC = 'CREATED_AT_DESC',
|
||||
CREATED_AT_ASC = 'CREATED_AT_ASC',
|
||||
REPLIES_DESC = 'REPLIES_DESC',
|
||||
RESPECT_DESC = 'RESPECT_DESC',
|
||||
}
|
||||
|
||||
export interface ConnectionInput {
|
||||
first: number;
|
||||
orderBy: CommentSort;
|
||||
after?: Cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* nodesToEdge converts a set of nodes and configuration options into a set of
|
||||
* edges.
|
||||
*
|
||||
* @param input connection configuration
|
||||
* @param nodes nodes returned from the query
|
||||
*/
|
||||
function nodesToEdge(
|
||||
input: ConnectionInput,
|
||||
nodes: Comment[]
|
||||
): Edge<Comment>[] {
|
||||
let getCursor: (comment: Comment, index: number) => Cursor;
|
||||
switch (input.orderBy) {
|
||||
case CommentSort.CREATED_AT_DESC:
|
||||
case CommentSort.CREATED_AT_ASC:
|
||||
getCursor = comment => comment.created_at;
|
||||
break;
|
||||
case CommentSort.REPLIES_DESC:
|
||||
case CommentSort.RESPECT_DESC:
|
||||
getCursor = (_, index) =>
|
||||
(input.after ? (input.after as number) : 0) + index + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
return nodes.map((comment, index) => ({
|
||||
node: comment,
|
||||
cursor: getCursor(comment, index),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieveRepliesConnection returns a Connection<Comment> for a given comments
|
||||
* replies.
|
||||
*
|
||||
* @param db database connection
|
||||
* @param parentID the parent id for the comment to retrieve
|
||||
* @param input connection configuration
|
||||
*/
|
||||
export async function retrieveRepliesConnection(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
assetID: string,
|
||||
parentID: string,
|
||||
input: ConnectionInput
|
||||
): Promise<Readonly<Connection<Comment>>> {
|
||||
// Create the query.
|
||||
const query = new Query(collection(db)).where({
|
||||
tenant_id: tenantID,
|
||||
asset_id: assetID,
|
||||
parent_id: parentID,
|
||||
});
|
||||
|
||||
// Return a connection for the comments query.
|
||||
return retrieveConnection(input, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieveAssetConnection returns a Connection<Comment> for a given Asset's
|
||||
* comments.
|
||||
*
|
||||
* @param db database connection
|
||||
* @param assetID the Asset id for the comment to retrieve
|
||||
* @param input connection configuration
|
||||
*/
|
||||
export async function retrieveAssetConnection(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
assetID: string,
|
||||
input: ConnectionInput
|
||||
): Promise<Readonly<Connection<Comment>>> {
|
||||
// Create the query.
|
||||
const query = new Query(collection(db)).where({
|
||||
tenant_id: tenantID,
|
||||
asset_id: assetID,
|
||||
parent_id: null,
|
||||
});
|
||||
|
||||
// Return a connection for the comments query.
|
||||
return retrieveConnection(input, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieveConnection returns a Connection<Comment> for the given input and
|
||||
* Query.
|
||||
*
|
||||
* @param input connection configuration
|
||||
* @param query the Query for the set of nodes that should have the connection
|
||||
* configuration applied
|
||||
*/
|
||||
async function retrieveConnection(
|
||||
input: ConnectionInput,
|
||||
query: Query<Comment>
|
||||
): Promise<Readonly<Connection<Comment>>> {
|
||||
// Apply some sorting options.
|
||||
switch (input.orderBy) {
|
||||
case CommentSort.CREATED_AT_DESC:
|
||||
query.orderBy({ created_at: -1 });
|
||||
if (input.after) {
|
||||
query.where({ created_at: { $lt: input.after as Date } });
|
||||
}
|
||||
break;
|
||||
case CommentSort.CREATED_AT_ASC:
|
||||
query.orderBy({ created_at: 1 });
|
||||
if (input.after) {
|
||||
query.where({ created_at: { $gt: input.after as Date } });
|
||||
}
|
||||
break;
|
||||
case CommentSort.REPLIES_DESC:
|
||||
query.orderBy({ reply_count: -1, created_at: -1 });
|
||||
if (input.after) {
|
||||
query.after(input.after as number);
|
||||
}
|
||||
break;
|
||||
case CommentSort.RESPECT_DESC:
|
||||
query.orderBy({ 'action_counts.respect': -1, created_at: -1 });
|
||||
if (input.after) {
|
||||
query.after(input.after as number);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// We load one more than the limit so we can determine if there is
|
||||
// another page of entries.
|
||||
query.first(input.first + 1);
|
||||
|
||||
// Get the cursor.
|
||||
const cursor = await query.exec();
|
||||
|
||||
// Get the comments from the cursor.
|
||||
const nodes = await cursor.toArray();
|
||||
|
||||
// The hasNextPage is always handled the same (ask for one more than we need,
|
||||
// if there is one more, than there is more).
|
||||
let hasNextPage = false;
|
||||
if (input.first >= 0 && nodes.length > input.first) {
|
||||
// There was one more than we expected! Set hasNextPage = true and remove
|
||||
// the last item from the array that we requested.
|
||||
hasNextPage = true;
|
||||
nodes.splice(input.first, 1);
|
||||
}
|
||||
|
||||
// Convert the nodes to edges.
|
||||
const edges = nodesToEdge(input, nodes);
|
||||
|
||||
// Return the connection.
|
||||
return {
|
||||
edges,
|
||||
pageInfo: {
|
||||
hasNextPage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export type Cursor = Date | number | string;
|
||||
|
||||
export interface Edge<T> {
|
||||
node: T;
|
||||
cursor: Cursor;
|
||||
}
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface Connection<T> {
|
||||
edges: Edge<T>[];
|
||||
pageInfo: PageInfo;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { merge } from 'lodash';
|
||||
import { Collection, Cursor } from 'mongodb';
|
||||
import { FilterQuery as MongoFilterQuery } from 'mongodb';
|
||||
import { Writeable } from '../../common/types';
|
||||
|
||||
/**
|
||||
* FilterQuery<T> ensures that given the type T, that the FilterQuery will be a
|
||||
* writeable, partial set of properties while also including MongoDB specific
|
||||
* properties (like $lt, or $gte).
|
||||
*/
|
||||
export type FilterQuery<T> = MongoFilterQuery<Writeable<Partial<T>>>;
|
||||
|
||||
/**
|
||||
* Query is a convenience class used to wrap the existing MongoDB driver to
|
||||
* provide easier complex query management.
|
||||
*/
|
||||
export default class Query<T> {
|
||||
public filter: FilterQuery<T>;
|
||||
|
||||
private collection: Collection<T>;
|
||||
private skip?: number;
|
||||
private limit?: number;
|
||||
private sort?: Object;
|
||||
|
||||
constructor(collection: Collection<T>) {
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* where will merge the given filter into the existing query.
|
||||
*
|
||||
* @param filter the filter to merge into the existing query
|
||||
*/
|
||||
public where(filter: FilterQuery<T>): Query<T> {
|
||||
this.filter = merge({}, this.filter || {}, filter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* after will skip the indicated number of documents.
|
||||
*
|
||||
* @param skip the number of documents to skip
|
||||
*/
|
||||
public after(skip: number): Query<T> {
|
||||
this.skip = skip;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* first will limit to the indicated number of documents.
|
||||
*
|
||||
* @param limit the number of documents to limit the result to
|
||||
*/
|
||||
public first(limit: number): Query<T> {
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* orderBy will apply sorting to the query filter when executed.
|
||||
*
|
||||
* @param sort the sorting option for the documents
|
||||
*/
|
||||
public orderBy(sort: Object): Query<T> {
|
||||
this.sort = merge({}, this.sort || {}, sort);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* exec will return a cursor to the query.
|
||||
*/
|
||||
async exec(): Promise<Cursor<T>> {
|
||||
let cursor = await this.collection.find(this.filter);
|
||||
|
||||
if (this.limit) {
|
||||
// Apply a limit if it exists.
|
||||
cursor = cursor.limit(this.limit);
|
||||
}
|
||||
|
||||
if (this.sort) {
|
||||
// Apply a sort if it exists.
|
||||
cursor = cursor.sort(this.sort);
|
||||
}
|
||||
|
||||
if (this.skip) {
|
||||
// Apply a skip if it exists.
|
||||
cursor = cursor.skip(this.skip);
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Db } from 'mongodb';
|
||||
import { Db, Collection } from 'mongodb';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
import dotize from 'dotize';
|
||||
import uuid from 'uuid';
|
||||
import { Omit } from 'talk-common/types';
|
||||
|
||||
// selector is the single document selector for the Settings model stored in the
|
||||
// settings collection in MongoDB.
|
||||
const selector = { id: '1' };
|
||||
function collection(db: Db): Collection<Tenant> {
|
||||
return db.collection<Tenant>('tenants');
|
||||
}
|
||||
|
||||
export interface TenantResource {
|
||||
readonly tenant_id: string;
|
||||
}
|
||||
|
||||
export interface Wordlist {
|
||||
banned: string[];
|
||||
@@ -16,7 +22,7 @@ export enum Moderation {
|
||||
POST = 'POST',
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
export interface Tenant {
|
||||
readonly id: string;
|
||||
|
||||
moderation: Moderation;
|
||||
@@ -49,10 +55,9 @@ export interface Settings {
|
||||
domains: string[];
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
// Include the selector.
|
||||
...selector,
|
||||
export type CreateTenantInput = Omit<Tenant, 'id'>;
|
||||
|
||||
const defaults: CreateTenantInput = {
|
||||
// Default to post moderation.
|
||||
moderation: Moderation.POST,
|
||||
|
||||
@@ -76,40 +81,48 @@ const defaultSettings: Settings = {
|
||||
|
||||
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,
|
||||
}
|
||||
);
|
||||
input: Partial<CreateTenantInput>
|
||||
): Promise<Readonly<Tenant>> {
|
||||
const tenant = defaultsDeep({ id: uuid.v4() }, input, defaults);
|
||||
|
||||
return result.value;
|
||||
await collection(db).insert(tenant);
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
export async function retrieve(db: Db, id: string): Promise<Readonly<Tenant>> {
|
||||
return collection(db).findOne({ id });
|
||||
}
|
||||
|
||||
return settings;
|
||||
export async function retrieveMany(
|
||||
db: Db,
|
||||
ids: string[]
|
||||
): Promise<Readonly<Tenant>[]> {
|
||||
const cursor = await collection(db).find({
|
||||
id: {
|
||||
$in: ids,
|
||||
},
|
||||
});
|
||||
|
||||
const tenants = await cursor.toArray();
|
||||
|
||||
return ids.map(id => tenants.find(tenant => tenant.id === id));
|
||||
}
|
||||
|
||||
export async function retrieveAll(db: Db): Promise<Readonly<Tenant>[]> {
|
||||
return collection(db)
|
||||
.find({})
|
||||
.toArray();
|
||||
}
|
||||
|
||||
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,
|
||||
id: string,
|
||||
update: Partial<CreateTenantInput>
|
||||
): Promise<Readonly<Tenant>> {
|
||||
// Get the tenant from the database.
|
||||
const result = await collection(db).findOneAndUpdate(
|
||||
{ id },
|
||||
// Only update fields that have been updated.
|
||||
{ $set: dotize(update) },
|
||||
// False to return the updated document instead of the original
|
||||
@@ -1,5 +0,0 @@
|
||||
import { FilterQuery } from 'mongodb';
|
||||
import { Writeable } from '../../common/types';
|
||||
|
||||
export type FilterQuery<T> = Writeable<Partial<T>> &
|
||||
FilterQuery<Writeable<Partial<T>>>;
|
||||
@@ -0,0 +1,181 @@
|
||||
import { ActionCounts } from 'talk-server/models/actions';
|
||||
import { Db, Collection } from 'mongodb';
|
||||
import uuid from 'uuid';
|
||||
import { Omit, Sub } from 'talk-common/types';
|
||||
import { merge } from 'lodash';
|
||||
import { TenantResource } from 'talk-server/models/tenant';
|
||||
|
||||
function collection(db: Db): Collection<User> {
|
||||
return db.collection<User>('users');
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
readonly id: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
readonly id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export enum UserUsernameStatus {
|
||||
// UNSET is used when the username can be changed, and does not necessarily
|
||||
// require moderator action to become active. This can be used when the user
|
||||
// signs up with a social login and has the option of setting their own
|
||||
// username.
|
||||
UNSET = 'UNSET',
|
||||
|
||||
// SET is used when the username has been set for the first time, but cannot
|
||||
// change without the username being rejected by a moderator and that moderator
|
||||
// agreeing that the username should be allowed to change.
|
||||
SET = 'SET',
|
||||
|
||||
// APPROVED is used when the username was changed, and subsequently approved by
|
||||
// said moderator.
|
||||
APPROVED = 'APPROVED',
|
||||
|
||||
// REJECTED is used when the username was changed, and subsequently rejected by
|
||||
// said moderator.
|
||||
REJECTED = 'REJECTED',
|
||||
|
||||
// CHANGED is used after a user has changed their username after it was
|
||||
// rejected.
|
||||
CHANGED = 'CHANGED',
|
||||
}
|
||||
|
||||
export enum UserRole {
|
||||
ADMIN = 'ADMIN',
|
||||
MODERATOR = 'MODERATOR',
|
||||
STAFF = 'STAFF',
|
||||
COMMENTER = 'COMMENTER',
|
||||
}
|
||||
|
||||
export interface UserStatusHistory<T> {
|
||||
status: T; // TODO: migrate field
|
||||
assigned_by?: string;
|
||||
reason?: string; // TODO: migrate field
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface UserStatusItem<T> {
|
||||
status: T; // TODO: migrate field
|
||||
history: UserStatusHistory<T>[];
|
||||
}
|
||||
|
||||
export interface UserStatus {
|
||||
username: UserStatusItem<UserUsernameStatus>;
|
||||
banned: UserStatusItem<boolean>;
|
||||
suspension: UserStatusItem<Date>;
|
||||
}
|
||||
|
||||
export interface User extends TenantResource {
|
||||
readonly id: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
profiles: Profile[];
|
||||
tokens: Token[];
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
action_counts: ActionCounts;
|
||||
ignored_users: string[]; // TODO: migrate field
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export type CreateUserInput = Omit<
|
||||
User,
|
||||
| 'id'
|
||||
| 'tenant_id'
|
||||
| 'tokens'
|
||||
| 'status'
|
||||
| 'role'
|
||||
| 'action_counts'
|
||||
| 'ignored_users'
|
||||
| 'created_at'
|
||||
>;
|
||||
|
||||
export async function create(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
input: CreateUserInput
|
||||
): Promise<Readonly<User>> {
|
||||
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 user is
|
||||
// created.
|
||||
const defaults: Sub<User, CreateUserInput> = {
|
||||
id: uuid.v4(),
|
||||
tenant_id: tenantID,
|
||||
role: UserRole.COMMENTER,
|
||||
tokens: [],
|
||||
action_counts: {},
|
||||
ignored_users: [],
|
||||
status: {
|
||||
banned: {
|
||||
status: false,
|
||||
history: [],
|
||||
},
|
||||
suspension: {
|
||||
status: null,
|
||||
history: [],
|
||||
},
|
||||
username: {
|
||||
status: UserUsernameStatus.SET,
|
||||
history: [],
|
||||
},
|
||||
},
|
||||
created_at: now,
|
||||
};
|
||||
|
||||
// Merge the defaults and the input together.
|
||||
const user: User = merge({}, defaults, input);
|
||||
|
||||
// Insert it into the database.
|
||||
await collection(db).insertOne(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function retrieve(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
id: string
|
||||
): Promise<Readonly<User>> {
|
||||
return collection(db).findOne({ id, tenant_id: tenantID });
|
||||
}
|
||||
|
||||
export async function retrieveMany(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
ids: string[]
|
||||
): Promise<Readonly<User>[]> {
|
||||
const cursor = await collection(db).find({
|
||||
id: {
|
||||
$in: ids,
|
||||
},
|
||||
tenant_id: tenantID,
|
||||
});
|
||||
|
||||
const users = await cursor.toArray();
|
||||
|
||||
return ids.map(id => users.find(comment => comment.id === id));
|
||||
}
|
||||
|
||||
export async function updateRole(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
role: UserRole
|
||||
): Promise<Readonly<User>> {
|
||||
const result = await collection(db).findOneAndUpdate(
|
||||
{ id, tenant_id: tenantID },
|
||||
{ $set: { role } },
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
Reference in New Issue
Block a user