-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
701 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import DataLoader from "dataloader"; | ||
import { Request } from "express"; | ||
import { Forbidden, Unauthorized } from "http-errors"; | ||
import db, { Identity, User } from "../db"; | ||
import { mapTo, mapToMany } from "./utils"; | ||
|
||
/** | ||
* GraphQL execution context. | ||
* @see https://graphql.org/learn/execution/ | ||
*/ | ||
export class Context { | ||
private readonly req: Request; | ||
|
||
constructor(req: Request) { | ||
this.req = req; | ||
|
||
// Add the currently logged in user object to the cache | ||
if (req.user) { | ||
this.userById.prime(req.user.id, req.user); | ||
if (req.user.username) { | ||
this.userByUsername.prime(req.user.username, req.user); | ||
} | ||
} | ||
} | ||
|
||
/* | ||
* Authentication and authorization | ||
* ------------------------------------------------------------------------ */ | ||
|
||
get user(): User | null { | ||
return this.req.user; | ||
} | ||
|
||
signIn(user: User | null | undefined): Promise<User | null> { | ||
return this.req.signIn(user); | ||
} | ||
|
||
signOut(): void { | ||
this.req.signOut(); | ||
} | ||
|
||
ensureAuthorized(check?: (user: User) => boolean): void { | ||
if (!this.req.user) { | ||
throw new Unauthorized(); | ||
} | ||
|
||
if (check && !check(this.req.user)) { | ||
throw new Forbidden(); | ||
} | ||
} | ||
|
||
/* | ||
* Data loaders | ||
* ------------------------------------------------------------------------ */ | ||
|
||
userById = new DataLoader<string, User | null>((keys) => | ||
db | ||
.table<User>("user") | ||
.whereIn("id", keys) | ||
.select() | ||
.then((rows) => | ||
rows.map((x) => { | ||
if (x.username) this.userByUsername.prime(x.username, x); | ||
return x; | ||
}) | ||
) | ||
.then((rows) => mapTo(rows, keys, (x) => x.id)) | ||
); | ||
|
||
userByUsername = new DataLoader<string, User | null>((keys) => | ||
db | ||
.table<User>("user") | ||
.whereIn("username", keys) | ||
.select() | ||
.then((rows) => | ||
rows.map((x) => { | ||
this.userById.prime(x.id, x); | ||
return x; | ||
}) | ||
) | ||
.then((rows) => mapTo(rows, keys, (x) => x.username)) | ||
); | ||
|
||
identitiesByUserId = new DataLoader<string, Identity[]>((keys) => | ||
db | ||
.table<Identity>("identity") | ||
.whereIn("user_id", keys) | ||
.select() | ||
.then((rows) => mapToMany(rows, keys, (x) => x.user_id)) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import type { Request } from "express"; | ||
import { graphqlHTTP } from "express-graphql"; | ||
import { formatError, GraphQLObjectType, GraphQLSchema } from "graphql"; | ||
import { reportError } from "../core"; | ||
import env from "../env"; | ||
import { Context } from "./context"; | ||
import * as mutations from "./mutations"; | ||
import * as queries from "./queries"; | ||
import { nodeField, nodesField } from "./types/node"; | ||
import { ValidationError } from "./utils"; | ||
|
||
export const schema = new GraphQLSchema({ | ||
query: new GraphQLObjectType({ | ||
name: "Root", | ||
description: "The top-level API", | ||
|
||
fields: { | ||
node: nodeField, | ||
nodes: nodesField, | ||
...queries, | ||
}, | ||
}), | ||
|
||
mutation: new GraphQLObjectType({ | ||
name: "Mutation", | ||
fields: mutations, | ||
}), | ||
}); | ||
|
||
/** | ||
* GraphQL middleware for Express.js | ||
*/ | ||
export const graphql = graphqlHTTP((req, res, params) => ({ | ||
schema, | ||
context: new Context(req as Request), | ||
graphiql: env.APP_ENV !== "prod", | ||
pretty: !env.isProduction, | ||
customFormatErrorFn: (err) => { | ||
if (err.originalError instanceof ValidationError) { | ||
return { ...formatError(err), errors: err.originalError.errors }; | ||
} | ||
|
||
reportError(err.originalError || err, req as Request, params); | ||
console.error(err.originalError || err); | ||
return formatError(err); | ||
}, | ||
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import { GraphQLFieldConfig, GraphQLString } from "graphql"; | ||
import { Context } from "../context"; | ||
|
||
export const signOut: GraphQLFieldConfig<unknown, Context> = { | ||
description: "Clears authentication session.", | ||
type: GraphQLString, | ||
|
||
resolve(self, args, ctx) { | ||
ctx.signOut(); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
export * from "./auth"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import { GraphQLFieldConfig, GraphQLInt, GraphQLNonNull } from "graphql"; | ||
import type { Knex } from "knex"; | ||
import { Context } from "../context"; | ||
|
||
export const countField: GraphQLFieldConfig< | ||
{ query: Knex.QueryBuilder }, | ||
Context | ||
> = { | ||
type: new GraphQLNonNull(GraphQLInt), | ||
|
||
async resolve(self) { | ||
const rows = await self.query.count(); | ||
return rows[0].count; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
export * from "./me"; | ||
export * from "./user"; | ||
export * from "./users"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import type { GraphQLFieldConfig } from "graphql"; | ||
import type { User } from "../../db"; | ||
import type { Context } from "../context"; | ||
import { UserType } from "../types"; | ||
|
||
export const me: GraphQLFieldConfig<User, Context> = { | ||
description: "The authenticated user.", | ||
type: UserType, | ||
|
||
resolve(self, args, ctx) { | ||
return ctx.user; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import { GraphQLFieldConfig, GraphQLString } from "graphql"; | ||
import type { User } from "../../db"; | ||
import db from "../../db"; | ||
import type { Context } from "../context"; | ||
import { UserType } from "../types"; | ||
|
||
export const user: GraphQLFieldConfig<User, Context> = { | ||
description: "Find user by username or email.", | ||
type: UserType, | ||
|
||
args: { | ||
username: { type: GraphQLString }, | ||
email: { type: GraphQLString }, | ||
}, | ||
|
||
resolve(self, args, ctx) { | ||
const query = db.table<User>("user"); | ||
|
||
if (args.username) { | ||
query.where("username", "=", args.username); | ||
} else if (args.email) { | ||
ctx.ensureAuthorized(); | ||
query.where("email", "=", args.email); | ||
} else { | ||
throw new Error("The username argument is required."); | ||
} | ||
|
||
return query.first(); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { GraphQLFieldConfig } from "graphql"; | ||
import { | ||
connectionDefinitions, | ||
connectionFromArraySlice, | ||
cursorToOffset, | ||
forwardConnectionArgs, | ||
} from "graphql-relay"; | ||
import db, { User } from "../../db"; | ||
import { Context } from "../context"; | ||
import { UserType } from "../types"; | ||
import { countField } from "./fields"; | ||
|
||
export const users: GraphQLFieldConfig<unknown, Context> = { | ||
type: connectionDefinitions({ | ||
name: "User", | ||
nodeType: UserType, | ||
connectionFields: { totalCount: countField }, | ||
}).connectionType, | ||
|
||
args: forwardConnectionArgs, | ||
|
||
async resolve(root, args, ctx) { | ||
// Only admins are allowed to fetch the list of user accounts. | ||
ctx.ensureAuthorized((user) => user.admin); | ||
|
||
const query = db.table<User>("user"); | ||
|
||
const limit = args.first === undefined ? 50 : args.first; | ||
const offset = args.after ? cursorToOffset(args.after) + 1 : 0; | ||
|
||
const data = await query | ||
.clone() | ||
.limit(limit) | ||
.offset(offset) | ||
.orderBy("created_at", "desc") | ||
.select(); | ||
|
||
data.forEach((x) => { | ||
ctx.userById.prime(x.id, x); | ||
if (x.username) ctx.userByUsername.prime(x.username, x); | ||
}); | ||
|
||
return { | ||
...connectionFromArraySlice(data, args, { | ||
sliceStart: offset, | ||
arrayLength: offset + data.length, | ||
}), | ||
query, | ||
}; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* SPDX-FileCopyrightText: 2016-present Kriasoft <[email protected]> */ | ||
/* SPDX-License-Identifier: MIT */ | ||
|
||
import { GraphQLEnumType } from "graphql"; | ||
import { mapValues } from "lodash"; | ||
import { IdentityProvider } from "../../db"; | ||
|
||
export const IdentityProviderType = new GraphQLEnumType({ | ||
name: "IdentityProvider", | ||
description: "OAuth identity provider.", | ||
values: mapValues(IdentityProvider, (value) => ({ value })), | ||
}); |
Oops, something went wrong.