mirror of
https://github.com/wassname/talk.git
synced 2026-07-23 13:10:20 +08:00
[CORL 133] API Review (#2197)
* refactor: removed unused subscription code
* refactor: removed management api's
* refactor: cleanup of connections
* refactor: refactored comments edge
* refactor: simplified connection resolving
* feat: added story connection edge
* fix: added story index
* feat: added user pagination and user edge
* fix: added filter to comment query
* fix: removed unused resolvers
* fix: creating a comment reply should require auth
* refactor: cleanup of graph files
* feat: removed display name, made username non-unique
* fix: fixed tests
* fix: fixed tests
* fix: added more api docs
* fix: fixed bug with installer
* refactor: fixes and updates
* fix: added linting for graphql, fixed schema
* feat: added docker build tests
* fix: upped output timeout
* fix: fixed stacktraces in production builds
* fix: removed `git add`
- `git add` was causing issues with
partial staged changs on files
* feat: improved error messaging for auth
* refactor: cleaned up queue names
* fix: merge error
This commit is contained in:
@@ -17,8 +17,8 @@ import {
|
||||
} from "talk-server/models/helpers/query";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
|
||||
function collection(db: Db) {
|
||||
return db.collection<Readonly<CommentAction>>("commentActions");
|
||||
function collection(mongo: Db) {
|
||||
return mongo.collection<Readonly<CommentAction>>("commentActions");
|
||||
}
|
||||
|
||||
export enum ACTION_TYPE {
|
||||
|
||||
@@ -6,8 +6,7 @@ import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__
|
||||
import {
|
||||
Connection,
|
||||
ConnectionInput,
|
||||
getPageInfo,
|
||||
nodesToEdges,
|
||||
resolveConnection,
|
||||
} from "talk-server/models/helpers/connection";
|
||||
import Query, {
|
||||
createConnectionOrderVariants,
|
||||
@@ -15,8 +14,8 @@ import Query, {
|
||||
} from "talk-server/models/helpers/query";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
|
||||
function collection(db: Db) {
|
||||
return db.collection<Readonly<CommentModerationAction>>(
|
||||
function collection(mongo: Db) {
|
||||
return mongo.collection<Readonly<CommentModerationAction>>(
|
||||
"commentModerationActions"
|
||||
);
|
||||
}
|
||||
@@ -157,34 +156,6 @@ async function retrieveConnection(
|
||||
query.where({ createdAt: { $lt: input.after as Date } });
|
||||
}
|
||||
|
||||
// We load one more than the limit so we can determine if there is
|
||||
// another page of entries. This gets trimmed off below after we've checked to
|
||||
// see if this constitutes another page of edges.
|
||||
query.first(input.first + 1);
|
||||
|
||||
// Get the cursor.
|
||||
const cursor = await query.exec();
|
||||
|
||||
// Get the comments from the cursor.
|
||||
const nodes = await cursor.toArray();
|
||||
|
||||
// Convert the nodes to edges (which will include the extra edge we don't need
|
||||
// if there is more results).
|
||||
const edges = nodesToEdges(nodes, a => a.createdAt);
|
||||
|
||||
// Get the pageInfo for the connection. We will use this to also determine if
|
||||
// we need to trim off the extra edge that we requested by comparing its
|
||||
// hasNextPage parameter.
|
||||
const pageInfo = getPageInfo(input, edges);
|
||||
if (pageInfo.hasNextPage) {
|
||||
// Because this means that we got one more than expected, we should trim off
|
||||
// the extra edge that was retrieved.
|
||||
edges.splice(input.first, 1);
|
||||
}
|
||||
|
||||
// Return the connection.
|
||||
return {
|
||||
edges,
|
||||
pageInfo,
|
||||
};
|
||||
// Return a connection.
|
||||
return resolveConnection(query, input, a => a.createdAt);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ import {
|
||||
import {
|
||||
Connection,
|
||||
createConnection,
|
||||
getPageInfo,
|
||||
doesNotContainNull,
|
||||
nodesToEdges,
|
||||
NodeToCursorTransformer,
|
||||
OrderedConnectionInput,
|
||||
resolveConnection,
|
||||
} from "talk-server/models/helpers/connection";
|
||||
import Query, {
|
||||
createConnectionOrderVariants,
|
||||
@@ -123,6 +124,11 @@ export interface Comment extends TenantResource {
|
||||
*/
|
||||
replyCount: number;
|
||||
|
||||
/**
|
||||
* metadata stores the deep Comment properties.
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* createdAt is the date that this Comment was created.
|
||||
*/
|
||||
@@ -133,11 +139,6 @@ export interface Comment extends TenantResource {
|
||||
* undefined, this Comment is not deleted.
|
||||
*/
|
||||
deletedAt?: Date;
|
||||
|
||||
/**
|
||||
* metadata stores the deep Comment properties.
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export async function createCommentIndexes(mongo: Db) {
|
||||
@@ -550,6 +551,7 @@ export async function retrieveCommentParentsConnection(
|
||||
|
||||
return {
|
||||
edges: [{ node: parent, cursor: 1 }],
|
||||
nodes: [parent],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: comment.grandparentIDs.length > 0,
|
||||
@@ -566,29 +568,26 @@ export async function retrieveCommentParentsConnection(
|
||||
const parentIDSubset = parentIDs.slice(skip, skip + limit);
|
||||
|
||||
// Retrieve the parents via the subset list.
|
||||
const parents = await retrieveManyComments(mongo, tenantID, parentIDSubset);
|
||||
const nodes = await retrieveManyComments(mongo, tenantID, parentIDSubset);
|
||||
|
||||
// Loop over the list to ensure that none of the entries is null (indicating
|
||||
// that there was a misplaced parent). We can assert the type here because we
|
||||
// will throw an error and abort if one of the comments are null.
|
||||
parents.forEach(parentComment => {
|
||||
if (!parentComment) {
|
||||
// TODO: (wyattjoh) replace with a better error.
|
||||
throw new Error("parent id specified does not exist");
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
if (!doesNotContainNull(nodes)) {
|
||||
// TODO: (wyattjoh) replace with a better error.
|
||||
throw new Error("parent id specified does not exist");
|
||||
}
|
||||
|
||||
const edges = nodesToEdges(
|
||||
// We can't have a null parent after the forEach filter above.
|
||||
parents as Array<Readonly<Comment>>,
|
||||
nodes,
|
||||
(_, index) => index + skip + 1
|
||||
).reverse();
|
||||
|
||||
// Return the resolved connection.
|
||||
return {
|
||||
edges,
|
||||
nodes,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: parentIDs.length > limit + skip,
|
||||
@@ -721,44 +720,8 @@ async function retrieveConnection(
|
||||
// Apply some sorting options.
|
||||
applyInputToQuery(input, query);
|
||||
|
||||
// We load one more than the limit so we can determine if there is
|
||||
// another page of entries. This gets trimmed off below after we've checked to
|
||||
// see if this constitutes another page of edges.
|
||||
query.first(input.first + 1);
|
||||
|
||||
// Get the cursor.
|
||||
const cursor = await query.exec();
|
||||
|
||||
// Get the comments from the cursor.
|
||||
const nodes = await cursor.toArray();
|
||||
|
||||
// Return a connection.
|
||||
return convertNodesToConnection(input, nodes);
|
||||
}
|
||||
|
||||
export function convertNodesToConnection(
|
||||
input: CommentConnectionInput,
|
||||
nodes: Array<Readonly<Comment>>
|
||||
) {
|
||||
// Convert the nodes to edges (which will include the extra edge we don't need
|
||||
// if there is more results).
|
||||
const edges = nodesToEdges(nodes, cursorGetterFactory(input));
|
||||
|
||||
// Get the pageInfo for the connection. We will use this to also determine if
|
||||
// we need to trim off the extra edge that we requested by comparing its
|
||||
// hasNextPage parameter.
|
||||
const pageInfo = getPageInfo(input, edges);
|
||||
if (pageInfo.hasNextPage) {
|
||||
// Because this means that we got one more than expected, we should trim off
|
||||
// the extra edge that was retrieved.
|
||||
edges.splice(input.first, 1);
|
||||
}
|
||||
|
||||
// Return the connection.
|
||||
return {
|
||||
edges,
|
||||
pageInfo,
|
||||
};
|
||||
return resolveConnection(query, input, cursorGetterFactory(input));
|
||||
}
|
||||
|
||||
function applyInputToQuery(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { merge } from "lodash";
|
||||
|
||||
import { FilterQuery } from "talk-server/models/helpers/query";
|
||||
import Query, { FilterQuery } from "talk-server/models/helpers/query";
|
||||
|
||||
export type Cursor = Date | number | string | null;
|
||||
|
||||
@@ -10,14 +10,15 @@ export interface Edge<T> {
|
||||
}
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
startCursor?: Cursor;
|
||||
endCursor?: Cursor;
|
||||
}
|
||||
|
||||
export interface Connection<T> {
|
||||
edges: Array<Edge<T>>;
|
||||
nodes: T[];
|
||||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ export function createConnection<T>(
|
||||
return merge(
|
||||
{
|
||||
edges: [],
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
@@ -117,3 +119,53 @@ export function nodesToEdges<T>(
|
||||
cursor: transformer(node, index),
|
||||
}));
|
||||
}
|
||||
|
||||
export function doesNotContainNull<T>(items: Array<T | null>): items is T[] {
|
||||
return items.every(item => Boolean(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* resolveConnection will transform a query, pagination args into a full typed
|
||||
* connection. This will add `1` to the length of elements being retrieved to
|
||||
* determine if there is another page of results to be loaded. The additional
|
||||
* node requested will be trimmed from the connection output.
|
||||
*
|
||||
* @param query the query to use when retrieving the documents.
|
||||
* @param input the pagination arguments
|
||||
* @param transformer the node transformer which converts a node to a custor
|
||||
*/
|
||||
export async function resolveConnection<T>(
|
||||
query: Query<T>,
|
||||
input: PaginationArgs,
|
||||
transformer: NodeToCursorTransformer<T>
|
||||
) {
|
||||
// We load one more than the limit so we can determine if there is another
|
||||
// page of entries. This gets trimmed off below after we've checked to see if
|
||||
// this constitutes another page of edges.
|
||||
query.first(input.first + 1);
|
||||
|
||||
// Get the nodes.
|
||||
const nodes = await query.exec().then(cursor => cursor.toArray());
|
||||
|
||||
// Convert the nodes to edges (which will include the extra edge we don't need
|
||||
// if there is more results).
|
||||
const edges = nodesToEdges(nodes, transformer);
|
||||
|
||||
// Get the pageInfo for the connection. We will use this to also determine if
|
||||
// we need to trim off the extra edge that we requested by comparing its
|
||||
// hasNextPage parameter.
|
||||
const pageInfo = getPageInfo(input, edges);
|
||||
if (pageInfo.hasNextPage) {
|
||||
// Because this means that we got one more than expected, we should trim off
|
||||
// the extra edge that was retrieved.
|
||||
edges.splice(input.first, 1);
|
||||
nodes.splice(input.first, 1);
|
||||
}
|
||||
|
||||
// Return the connection.
|
||||
return {
|
||||
edges,
|
||||
nodes,
|
||||
pageInfo,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { merge } from "lodash";
|
||||
import { isUndefined, merge, omitBy } from "lodash";
|
||||
|
||||
import {
|
||||
Collection,
|
||||
Cursor,
|
||||
@@ -38,7 +39,7 @@ export default class Query<T> {
|
||||
* @param filter the filter to merge into the existing query
|
||||
*/
|
||||
public where(filter: FilterQuery<T>): Query<T> {
|
||||
this.filter = merge({}, this.filter || {}, filter);
|
||||
this.filter = merge({}, this.filter || {}, omitBy(filter, isUndefined));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -76,6 +77,17 @@ export default class Query<T> {
|
||||
* exec will return a cursor to the query.
|
||||
*/
|
||||
public async exec(): Promise<Cursor<T>> {
|
||||
logger.trace(
|
||||
{
|
||||
collection: this.collection.collectionName,
|
||||
filter: this.filter,
|
||||
limit: this.limit,
|
||||
sort: this.sort,
|
||||
skip: this.skip,
|
||||
},
|
||||
"executing query"
|
||||
);
|
||||
|
||||
let cursor = await this.collection.find(this.filter);
|
||||
|
||||
if (this.limit) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Omit } from "talk-common/types";
|
||||
import {
|
||||
GQLAuthDisplayNameConfiguration,
|
||||
GQLCharCount,
|
||||
GQLDisableCommenting,
|
||||
GQLEmail,
|
||||
@@ -71,12 +70,6 @@ export interface Auth {
|
||||
* authentication solutions.
|
||||
*/
|
||||
integrations: AuthIntegrations;
|
||||
|
||||
/**
|
||||
* displayName contains configuration related to the use of Display Names
|
||||
* across AuthIntegrations.
|
||||
*/
|
||||
displayName: GQLAuthDisplayNameConfiguration;
|
||||
}
|
||||
|
||||
export interface Settings extends ModerationSettings {
|
||||
|
||||
@@ -20,8 +20,8 @@ import { updateSharedCommentCounts } from "./shared";
|
||||
* collection provides a reference to the stories collection used by the
|
||||
* counting system.
|
||||
*/
|
||||
function collection<T = Story>(db: Db) {
|
||||
return db.collection<Readonly<T>>("stories");
|
||||
function collection<T = Story>(mongo: Db) {
|
||||
return mongo.collection<Readonly<T>>("stories");
|
||||
}
|
||||
|
||||
export async function createStoryCountIndexes(mongo: Db) {
|
||||
|
||||
@@ -47,8 +47,8 @@ const commentCountsModerationQueueQueuesKey = (tenantID: string) =>
|
||||
* collection provides a reference to the stories collection used by the
|
||||
* counting system.
|
||||
*/
|
||||
function collection<T = Story>(db: Db) {
|
||||
return db.collection<Readonly<T>>("stories");
|
||||
function collection<T = Story>(mongo: Db) {
|
||||
return mongo.collection<Readonly<T>>("stories");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,10 +5,18 @@ import { Omit } from "talk-common/types";
|
||||
import { dotize } from "talk-common/utils/dotize";
|
||||
import { DuplicateStoryURLError } from "talk-server/errors";
|
||||
import { GQLStoryMetadata } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { createIndexFactory } from "talk-server/models/helpers/query";
|
||||
import Query, {
|
||||
createConnectionOrderVariants,
|
||||
createIndexFactory,
|
||||
} from "talk-server/models/helpers/query";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
|
||||
import {
|
||||
Connection,
|
||||
ConnectionInput,
|
||||
resolveConnection,
|
||||
} from "../helpers/connection";
|
||||
import {
|
||||
createEmptyCommentModerationQueueCounts,
|
||||
createEmptyCommentStatusCounts,
|
||||
@@ -18,8 +26,8 @@ import {
|
||||
// Export everything under counts.
|
||||
export * from "./counts";
|
||||
|
||||
function collection<T = Story>(db: Db) {
|
||||
return db.collection<Readonly<T>>("stories");
|
||||
function collection<T = Story>(mongo: Db) {
|
||||
return mongo.collection<Readonly<T>>("stories");
|
||||
}
|
||||
|
||||
export interface Story extends TenantResource {
|
||||
@@ -71,6 +79,17 @@ export async function createStoryIndexes(mongo: Db) {
|
||||
|
||||
// UNIQUE { url }
|
||||
await createIndex({ tenantID: 1, url: 1 }, { unique: true });
|
||||
|
||||
const variants = createConnectionOrderVariants<Readonly<Story>>([
|
||||
{ createdAt: -1 },
|
||||
]);
|
||||
|
||||
// Story based Comment Connection pagination.
|
||||
// { closedAt, ...connectionParams }
|
||||
await variants(createIndex, {
|
||||
tenantID: 1,
|
||||
closedAt: 1,
|
||||
});
|
||||
}
|
||||
|
||||
export interface UpsertStoryInput {
|
||||
@@ -79,7 +98,7 @@ export interface UpsertStoryInput {
|
||||
}
|
||||
|
||||
export async function upsertStory(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
{ id, url }: UpsertStoryInput
|
||||
) {
|
||||
@@ -103,7 +122,7 @@ export async function upsertStory(
|
||||
|
||||
// Perform the find and update operation to try and find and or create the
|
||||
// story.
|
||||
const result = await collection(db).findOneAndUpdate(
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
url,
|
||||
tenantID,
|
||||
@@ -128,21 +147,21 @@ export interface FindOrCreateStoryInput {
|
||||
}
|
||||
|
||||
export async function findOrCreateStory(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
{ id, url }: FindOrCreateStoryInput
|
||||
) {
|
||||
if (id) {
|
||||
if (url) {
|
||||
// The URL was specified, this is an upsert operation.
|
||||
return upsertStory(db, tenantID, {
|
||||
return upsertStory(mongo, tenantID, {
|
||||
id,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
// The URL was not specified, this is a lookup operation.
|
||||
return retrieveStory(db, tenantID, id);
|
||||
return retrieveStory(mongo, tenantID, id);
|
||||
}
|
||||
|
||||
// The ID was not specified, this is an upsert operation. Check to see that
|
||||
@@ -151,10 +170,10 @@ export async function findOrCreateStory(
|
||||
throw new Error("cannot upsert an story without the url");
|
||||
}
|
||||
|
||||
return upsertStory(db, tenantID, { url });
|
||||
return upsertStory(mongo, tenantID, { url });
|
||||
}
|
||||
|
||||
export type CreateStoryInput = Partial<Pick<Story, "metadata">>;
|
||||
export type CreateStoryInput = Partial<Pick<Story, "metadata" | "scrapedAt">>;
|
||||
|
||||
export async function createStory(
|
||||
mongo: Db,
|
||||
@@ -197,23 +216,23 @@ export async function createStory(
|
||||
}
|
||||
|
||||
export async function retrieveStoryByURL(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
url: string
|
||||
) {
|
||||
return collection(db).findOne({ url, tenantID });
|
||||
return collection(mongo).findOne({ url, tenantID });
|
||||
}
|
||||
|
||||
export async function retrieveStory(db: Db, tenantID: string, id: string) {
|
||||
return collection(db).findOne({ id, tenantID });
|
||||
export async function retrieveStory(mongo: Db, tenantID: string, id: string) {
|
||||
return collection(mongo).findOne({ id, tenantID });
|
||||
}
|
||||
|
||||
export async function retrieveManyStories(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
ids: string[]
|
||||
) {
|
||||
const cursor = await collection(db).find({
|
||||
const cursor = await collection(mongo).find({
|
||||
id: { $in: ids },
|
||||
tenantID,
|
||||
});
|
||||
@@ -224,11 +243,11 @@ export async function retrieveManyStories(
|
||||
}
|
||||
|
||||
export async function retrieveManyStoriesByURL(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
urls: string[]
|
||||
) {
|
||||
const cursor = await collection(db).find({
|
||||
const cursor = await collection(mongo).find({
|
||||
url: { $in: urls },
|
||||
tenantID,
|
||||
});
|
||||
@@ -244,7 +263,7 @@ export type UpdateStoryInput = Omit<
|
||||
>;
|
||||
|
||||
export async function updateStory(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
input: UpdateStoryInput
|
||||
@@ -259,7 +278,7 @@ export async function updateStory(
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await collection(db).findOneAndUpdate(
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id, tenantID },
|
||||
update,
|
||||
// False to return the updated document instead of the original
|
||||
@@ -304,3 +323,35 @@ export async function removeStories(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type StoryConnectionInput = ConnectionInput<Story>;
|
||||
|
||||
export async function retrieveStoryConnection(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
input: StoryConnectionInput
|
||||
): Promise<Readonly<Connection<Readonly<Story>>>> {
|
||||
// Create the query.
|
||||
const query = new Query(collection(mongo)).where({ tenantID });
|
||||
|
||||
// If a filter is being applied, filter it as well.
|
||||
if (input.filter) {
|
||||
query.where(input.filter);
|
||||
}
|
||||
|
||||
return retrieveConnection(input, query);
|
||||
}
|
||||
|
||||
async function retrieveConnection(
|
||||
input: StoryConnectionInput,
|
||||
query: Query<Story>
|
||||
): Promise<Readonly<Connection<Readonly<Story>>>> {
|
||||
// Apply the pagination arguments to the query.
|
||||
query.orderBy({ createdAt: -1 });
|
||||
if (input.after) {
|
||||
query.where({ createdAt: { $lt: input.after as Date } });
|
||||
}
|
||||
|
||||
// Return a connection.
|
||||
return resolveConnection(query, input, story => story.createdAt);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { GQLMODERATION_MODE } from "talk-server/graph/tenant/schema/__generated_
|
||||
import { createIndexFactory } from "talk-server/models/helpers/query";
|
||||
import { Settings } from "talk-server/models/settings";
|
||||
|
||||
function collection(db: Db) {
|
||||
return db.collection<Readonly<Tenant>>("tenants");
|
||||
function collection(mongo: Db) {
|
||||
return mongo.collection<Readonly<Tenant>>("tenants");
|
||||
}
|
||||
|
||||
export interface TenantResource {
|
||||
@@ -110,9 +110,6 @@ export async function createTenant(mongo: Db, input: CreateTenantInput) {
|
||||
banned: [],
|
||||
},
|
||||
auth: {
|
||||
displayName: {
|
||||
enabled: false,
|
||||
},
|
||||
integrations: {
|
||||
local: {
|
||||
enabled: true,
|
||||
@@ -200,16 +197,16 @@ export async function createTenant(mongo: Db, input: CreateTenantInput) {
|
||||
return tenant;
|
||||
}
|
||||
|
||||
export async function retrieveTenantByDomain(db: Db, domain: string) {
|
||||
return collection(db).findOne({ domain });
|
||||
export async function retrieveTenantByDomain(mongo: Db, domain: string) {
|
||||
return collection(mongo).findOne({ domain });
|
||||
}
|
||||
|
||||
export async function retrieveTenant(db: Db, id: string) {
|
||||
return collection(db).findOne({ id });
|
||||
export async function retrieveTenant(mongo: Db, id: string) {
|
||||
return collection(mongo).findOne({ id });
|
||||
}
|
||||
|
||||
export async function retrieveManyTenants(db: Db, ids: string[]) {
|
||||
const cursor = await collection(db).find({
|
||||
export async function retrieveManyTenants(mongo: Db, ids: string[]) {
|
||||
const cursor = await collection(mongo).find({
|
||||
id: {
|
||||
$in: ids,
|
||||
},
|
||||
@@ -220,8 +217,11 @@ export async function retrieveManyTenants(db: Db, ids: string[]) {
|
||||
return ids.map(id => tenants.find(tenant => tenant.id === id) || null);
|
||||
}
|
||||
|
||||
export async function retrieveManyTenantsByDomain(db: Db, domains: string[]) {
|
||||
const cursor = await collection(db).find({
|
||||
export async function retrieveManyTenantsByDomain(
|
||||
mongo: Db,
|
||||
domains: string[]
|
||||
) {
|
||||
const cursor = await collection(mongo).find({
|
||||
domain: {
|
||||
$in: domains,
|
||||
},
|
||||
@@ -234,8 +234,8 @@ export async function retrieveManyTenantsByDomain(db: Db, domains: string[]) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function retrieveAllTenants(db: Db) {
|
||||
return collection(db)
|
||||
export async function retrieveAllTenants(mongo: Db) {
|
||||
return collection(mongo)
|
||||
.find({})
|
||||
.toArray();
|
||||
}
|
||||
@@ -249,12 +249,12 @@ export async function countTenants(mongo: Db) {
|
||||
export type UpdateTenantInput = Omit<DeepPartial<Tenant>, "id" | "domain">;
|
||||
|
||||
export async function updateTenant(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
id: string,
|
||||
update: UpdateTenantInput
|
||||
) {
|
||||
// Get the tenant from the database.
|
||||
const result = await collection(db).findOneAndUpdate(
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id },
|
||||
// Only update fields that have been updated.
|
||||
{ $set: dotize(update, { embedArrays: true }) },
|
||||
@@ -279,7 +279,7 @@ function generateSSOKey() {
|
||||
* for the specified Tenant. All existing user sessions signed with the old
|
||||
* secret will be invalidated.
|
||||
*/
|
||||
export async function regenerateTenantSSOKey(db: Db, id: string) {
|
||||
export async function regenerateTenantSSOKey(mongo: Db, id: string) {
|
||||
// Construct the update.
|
||||
const update: DeepPartial<Tenant> = {
|
||||
auth: {
|
||||
@@ -293,7 +293,7 @@ export async function regenerateTenantSSOKey(db: Db, id: string) {
|
||||
};
|
||||
|
||||
// Update the Tenant with this new key.
|
||||
const result = await collection(db).findOneAndUpdate(
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id },
|
||||
// Serialize the deep update into the Tenant.
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Omit, Sub } from "talk-common/types";
|
||||
import {
|
||||
DuplicateEmailError,
|
||||
DuplicateUserError,
|
||||
DuplicateUsernameError,
|
||||
LocalProfileAlreadySetError,
|
||||
LocalProfileNotSetError,
|
||||
TokenNotFoundError,
|
||||
@@ -14,11 +13,17 @@ import {
|
||||
UserNotFoundError,
|
||||
} from "talk-server/errors";
|
||||
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
import Query, {
|
||||
createConnectionOrderVariants,
|
||||
createIndexFactory,
|
||||
FilterQuery,
|
||||
} from "talk-server/models/helpers/query";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
import {
|
||||
Connection,
|
||||
ConnectionInput,
|
||||
resolveConnection,
|
||||
} from "./helpers/connection";
|
||||
|
||||
function collection(mongo: Db) {
|
||||
return mongo.collection<Readonly<User>>("users");
|
||||
@@ -68,8 +73,6 @@ export interface Token {
|
||||
export interface User extends TenantResource {
|
||||
readonly id: string;
|
||||
username?: string;
|
||||
lowercaseUsername?: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
@@ -85,15 +88,6 @@ export async function createUserIndexes(mongo: Db) {
|
||||
// UNIQUE { id }
|
||||
await createIndex({ tenantID: 1, id: 1 }, { unique: true });
|
||||
|
||||
// UNIQUE - PARTIAL { lowercaseUsername }
|
||||
await createIndex(
|
||||
{ tenantID: 1, lowercaseUsername: 1 },
|
||||
{
|
||||
unique: true,
|
||||
partialFilterExpression: { lowercaseUsername: { $exists: true } },
|
||||
}
|
||||
);
|
||||
|
||||
// UNIQUE - PARTIAL { email }
|
||||
await createIndex(
|
||||
{ tenantID: 1, email: 1 },
|
||||
@@ -105,6 +99,17 @@ export async function createUserIndexes(mongo: Db) {
|
||||
{ tenantID: 1, "profiles.type": 1, "profiles.id": 1 },
|
||||
{ unique: true }
|
||||
);
|
||||
|
||||
const variants = createConnectionOrderVariants<Readonly<User>>([
|
||||
{ createdAt: -1 },
|
||||
]);
|
||||
|
||||
// Story based Comment Connection pagination.
|
||||
// { role, ...connectionParams }
|
||||
await variants(createIndex, {
|
||||
tenantID: 1,
|
||||
role: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function hashPassword(password: string): Promise<string> {
|
||||
@@ -113,11 +118,11 @@ function hashPassword(password: string): Promise<string> {
|
||||
|
||||
export type UpsertUserInput = Omit<
|
||||
User,
|
||||
"id" | "tenantID" | "tokens" | "createdAt" | "lowercaseUsername"
|
||||
"id" | "tenantID" | "tokens" | "createdAt"
|
||||
>;
|
||||
|
||||
export async function upsertUser(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
input: UpsertUserInput
|
||||
) {
|
||||
@@ -152,11 +157,6 @@ export async function upsertUser(
|
||||
profiles.push(profile);
|
||||
}
|
||||
|
||||
// Add in the lowercase username if it was sent.
|
||||
if (input.username) {
|
||||
defaults.lowercaseUsername = input.username.toLowerCase();
|
||||
}
|
||||
|
||||
// Merge the defaults and the input together.
|
||||
const user: Readonly<User> = {
|
||||
...defaults,
|
||||
@@ -176,7 +176,7 @@ export async function upsertUser(
|
||||
};
|
||||
|
||||
// Insert it into the database. This may throw an error.
|
||||
const result = await collection(db).findOneAndUpdate(filter, update, {
|
||||
const result = await collection(mongo).findOneAndUpdate(filter, update, {
|
||||
// We are using this to create a user, so we need to upsert it.
|
||||
upsert: true,
|
||||
|
||||
@@ -210,16 +210,16 @@ const createUpsertUserFilter = (user: Readonly<User>) => {
|
||||
return query;
|
||||
};
|
||||
|
||||
export async function retrieveUser(db: Db, tenantID: string, id: string) {
|
||||
return collection(db).findOne({ tenantID, id });
|
||||
export async function retrieveUser(mongo: Db, tenantID: string, id: string) {
|
||||
return collection(mongo).findOne({ tenantID, id });
|
||||
}
|
||||
|
||||
export async function retrieveManyUsers(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
ids: string[]
|
||||
) {
|
||||
const cursor = await collection(db).find({
|
||||
const cursor = await collection(mongo).find({
|
||||
id: {
|
||||
$in: ids,
|
||||
},
|
||||
@@ -232,11 +232,11 @@ export async function retrieveManyUsers(
|
||||
}
|
||||
|
||||
export async function retrieveUserWithProfile(
|
||||
db: Db,
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
profile: Partial<Pick<Profile, "id" | "type">>
|
||||
) {
|
||||
return collection(db).findOne({
|
||||
return collection(mongo).findOne({
|
||||
tenantID,
|
||||
profiles: {
|
||||
$elemMatch: profile,
|
||||
@@ -350,17 +350,7 @@ export async function setUserUsername(
|
||||
id: string,
|
||||
username: string
|
||||
) {
|
||||
// Lowercase the username.
|
||||
const lowercaseUsername = username.toLowerCase();
|
||||
|
||||
// Search to see if this username has been used before.
|
||||
let user = await collection(mongo).findOne({
|
||||
tenantID,
|
||||
lowercaseUsername,
|
||||
});
|
||||
if (user) {
|
||||
throw new DuplicateUsernameError(username);
|
||||
}
|
||||
// TODO: (wyattjoh) investigate adding the username previously used to an array.
|
||||
|
||||
// The username wasn't found, so add it to the user.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
@@ -372,7 +362,6 @@ export async function setUserUsername(
|
||||
{
|
||||
$set: {
|
||||
username,
|
||||
lowercaseUsername,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -383,7 +372,7 @@ export async function setUserUsername(
|
||||
);
|
||||
if (!result.value) {
|
||||
// Try to get the current user to discover what happened.
|
||||
user = await retrieveUser(mongo, tenantID, id);
|
||||
const user = await retrieveUser(mongo, tenantID, id);
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(id);
|
||||
}
|
||||
@@ -412,17 +401,7 @@ export async function updateUserUsername(
|
||||
id: string,
|
||||
username: string
|
||||
) {
|
||||
// Lowercase the username.
|
||||
const lowercaseUsername = username.toLowerCase();
|
||||
|
||||
// Search to see if this username has been used before.
|
||||
let user = await collection(mongo).findOne({
|
||||
tenantID,
|
||||
lowercaseUsername,
|
||||
});
|
||||
if (user) {
|
||||
throw new DuplicateUsernameError(username);
|
||||
}
|
||||
// TODO: (wyattjoh) investigate adding the username previously used to an array.
|
||||
|
||||
// The username wasn't found, so add it to the user.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
@@ -433,54 +412,6 @@ export async function updateUserUsername(
|
||||
{
|
||||
$set: {
|
||||
username,
|
||||
lowercaseUsername,
|
||||
},
|
||||
},
|
||||
{
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
if (!result.value) {
|
||||
// Try to get the current user to discover what happened.
|
||||
user = await retrieveUser(mongo, tenantID, id);
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(id);
|
||||
}
|
||||
|
||||
throw new Error("an unexpected error occured");
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* updateUserDisplayName will set the displayName of the User. If the display
|
||||
* name is not provided, it will be unset.
|
||||
*
|
||||
* @param mongo the database handle
|
||||
* @param tenantID the ID to the Tenant
|
||||
* @param id the ID of the User where we are setting the displayName on
|
||||
* @param displayName the displayName that we want to set
|
||||
*/
|
||||
export async function updateUserDisplayName(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
displayName?: string
|
||||
) {
|
||||
// The username wasn't found, so add it to the user.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
tenantID,
|
||||
id,
|
||||
},
|
||||
{
|
||||
// This will ensure that if the display name isn't provided, it will unset
|
||||
// the display name on the User.
|
||||
[displayName ? "$set" : "$unset"]: {
|
||||
displayName: displayName ? displayName : 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -832,3 +763,35 @@ export async function deactivateUserToken(
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
export type UserConnectionInput = ConnectionInput<User>;
|
||||
|
||||
export async function retrieveUserConnection(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
input: UserConnectionInput
|
||||
): Promise<Readonly<Connection<Readonly<User>>>> {
|
||||
// Create the query.
|
||||
const query = new Query(collection(mongo)).where({ tenantID });
|
||||
|
||||
// If a filter is being applied, filter it as well.
|
||||
if (input.filter) {
|
||||
query.where(input.filter);
|
||||
}
|
||||
|
||||
return retrieveConnection(input, query);
|
||||
}
|
||||
|
||||
async function retrieveConnection(
|
||||
input: UserConnectionInput,
|
||||
query: Query<User>
|
||||
): Promise<Readonly<Connection<Readonly<User>>>> {
|
||||
// Apply the pagination arguments to the query.
|
||||
query.orderBy({ createdAt: -1 });
|
||||
if (input.after) {
|
||||
query.where({ createdAt: { $lt: input.after as Date } });
|
||||
}
|
||||
|
||||
// Return a connection.
|
||||
return resolveConnection(query, input, user => user.createdAt);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user