[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:
Wyatt Johnson
2019-03-12 15:12:21 +01:00
committed by Kiwi
parent 37959f9398
commit d37333be89
125 changed files with 1269 additions and 1536 deletions
+2 -2
View File
@@ -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);
}
+16 -53
View File
@@ -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(
+55 -3
View File
@@ -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,
};
}
+14 -2
View File
@@ -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) {
-7
View File
@@ -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 {
+2 -2
View File
@@ -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");
}
/**
+71 -20
View File
@@ -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);
}
+19 -19
View File
@@ -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.
{
+62 -99
View File
@@ -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);
}