Skip to content

Commit e00ee51

Browse files
authored
Merge pull request #122 from Perlmint/feature/errors-plugin
2 parents 039a49e + b124ed4 commit e00ee51

File tree

13 files changed

+268
-84
lines changed

13 files changed

+268
-84
lines changed

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@pothos/core": "npm:@pothos/core@^4.7.3",
4343
"@pothos/plugin-complexity": "npm:@pothos/[email protected]",
4444
"@pothos/plugin-drizzle": "npm:@pothos/plugin-drizzle@^0.11.1",
45+
"@pothos/plugin-errors": "npm:@pothos/plugin-errors@^4.4.2",
4546
"@pothos/plugin-relay": "npm:@pothos/plugin-relay@^4.6.2",
4647
"@pothos/plugin-scope-auth": "npm:@pothos/plugin-scope-auth@^4.1.5",
4748
"@pothos/plugin-simple-objects": "npm:@pothos/plugin-simple-objects@^4.1.3",

deno.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

graphql/builder.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { Uuid } from "@hackerspub/models/uuid";
99
import SchemaBuilder from "@pothos/core";
1010
import ComplexityPlugin from "@pothos/plugin-complexity";
1111
import DrizzlePlugin from "@pothos/plugin-drizzle";
12+
import ErrorsPlugin from "@pothos/plugin-errors";
1213
import RelayPlugin from "@pothos/plugin-relay";
1314
import ScopeAuthPlugin from "@pothos/plugin-scope-auth";
1415
import SimpleObjectsPlugin from "@pothos/plugin-simple-objects";
@@ -29,6 +30,9 @@ import {
2930
import { createGraphQLError } from "graphql-yoga";
3031
import type Keyv from "keyv";
3132

33+
export type ValuesOfEnumType<T> = T extends
34+
PothosSchemaTypes.EnumRef<never, unknown, infer V> ? V : never;
35+
3236
export interface ServerContext {
3337
db: Database;
3438
kv: Keyv;
@@ -114,6 +118,7 @@ export const builder = new SchemaBuilder<PothosTypes>({
114118
SimpleObjectsPlugin,
115119
TracingPlugin,
116120
WithInputPlugin,
121+
ErrorsPlugin,
117122
],
118123
complexity: {
119124
defaultComplexity: 1,
@@ -148,6 +153,16 @@ export const builder = new SchemaBuilder<PothosTypes>({
148153
relay: {
149154
clientMutationId: "optional",
150155
},
156+
errors: {
157+
directResult: true,
158+
defaultUnionOptions: {
159+
name(options) {
160+
return `${options.fieldName.charAt(0).toUpperCase()}${
161+
options.fieldName.slice(1)
162+
}Result`;
163+
},
164+
},
165+
},
151166
});
152167

153168
builder.addScalarType("Date", DateResolver);

graphql/login.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,24 @@ import { sql } from "drizzle-orm";
2020
import { parseTemplate } from "url-template";
2121
import { Account } from "./account.ts";
2222
import { builder } from "./builder.ts";
23-
import { SessionRef } from "./session.ts";
2423
import { EMAIL_FROM } from "./email.ts";
24+
import { SessionRef } from "./session.ts";
2525

2626
const logger = getLogger(["hackerspub", "graphql", "login"]);
2727

28+
class AccountNotFoundError extends Error {
29+
public constructor(public readonly query: string) {
30+
super(`Account not found`);
31+
}
32+
}
33+
34+
builder.objectType(AccountNotFoundError, {
35+
name: "AccountNotFoundError",
36+
fields: (t) => ({
37+
query: t.exposeString("query"),
38+
}),
39+
});
40+
2841
interface LoginChallenge {
2942
accountId: Uuid;
3043
token: Uuid;
@@ -54,7 +67,15 @@ LoginChallengeRef.implement({
5467
builder.mutationFields((t) => ({
5568
loginByUsername: t.field({
5669
type: LoginChallengeRef,
57-
nullable: true,
70+
errors: {
71+
types: [AccountNotFoundError],
72+
union: {
73+
name: "LoginResult",
74+
},
75+
result: {
76+
name: "LoginSuccess",
77+
},
78+
},
5879
args: {
5980
username: t.arg.string({
6081
required: true,
@@ -80,7 +101,9 @@ builder.mutationFields((t) => ({
80101
with: { emails: true },
81102
where: { username: args.username },
82103
});
83-
if (account == null) return null;
104+
if (account == null) {
105+
throw new AccountNotFoundError(args.username);
106+
}
84107
const token = await createSigninToken(ctx.kv, account.id);
85108
const messages: Message[] = [];
86109
for (const { email } of account.emails) {
@@ -110,7 +133,15 @@ builder.mutationFields((t) => ({
110133

111134
loginByEmail: t.field({
112135
type: LoginChallengeRef,
113-
nullable: true,
136+
errors: {
137+
types: [AccountNotFoundError],
138+
union: {
139+
name: "LoginResult",
140+
},
141+
result: {
142+
name: "LoginSuccess",
143+
},
144+
},
114145
args: {
115146
email: t.arg.string({
116147
required: true,
@@ -150,7 +181,9 @@ builder.mutationFields((t) => ({
150181
with: { emails: true },
151182
});
152183
}
153-
if (account == null) return null;
184+
if (account == null) {
185+
throw new AccountNotFoundError(args.email);
186+
}
154187
const token = await createSigninToken(ctx.kv, account.id);
155188
const messages: Message[] = [];
156189
for (const { email } of account.emails) {

graphql/post.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Account } from "./account.ts";
1111
import { Actor } from "./actor.ts";
1212
import { builder, Node } from "./builder.ts";
1313
import { Reactable } from "./reactable.ts";
14+
import { NotAuthenticatedError } from "./session.ts";
1415

1516
const PostVisibility = builder.enumType("PostVisibility", {
1617
values: [
@@ -22,6 +23,19 @@ const PostVisibility = builder.enumType("PostVisibility", {
2223
] as const,
2324
});
2425

26+
class InvalidInputError extends Error {
27+
public constructor(public readonly inputPath: string) {
28+
super(`Invalid input - ${inputPath}`);
29+
}
30+
}
31+
32+
builder.objectType(InvalidInputError, {
33+
name: "InvalidInputError",
34+
fields: (t) => ({
35+
inputPath: t.expose("inputPath", { type: "String" }),
36+
}),
37+
});
38+
2539
export const Post = builder.drizzleInterface("postTable", {
2640
variant: "Post",
2741
interfaces: [Reactable, Node],
@@ -458,10 +472,16 @@ builder.relayMutationField(
458472
}),
459473
},
460474
{
475+
errors: {
476+
types: [
477+
NotAuthenticatedError,
478+
InvalidInputError,
479+
],
480+
},
461481
async resolve(_root, args, ctx) {
462482
const session = await ctx.session;
463483
if (session == null) {
464-
throw new Error("Not authenticated.");
484+
throw new NotAuthenticatedError();
465485
}
466486
const { visibility, content, language, replyTargetId, quotedPostId } =
467487
args.input;
@@ -472,7 +492,7 @@ builder.relayMutationField(
472492
where: { id: replyTargetId.id },
473493
});
474494
if (replyTarget == null) {
475-
throw new Error("Reply target not found.");
495+
throw new InvalidInputError("replyTargetId");
476496
}
477497
}
478498
let quotedPost: schema.Post & { actor: schema.Actor } | undefined;
@@ -482,7 +502,7 @@ builder.relayMutationField(
482502
where: { id: quotedPostId.id },
483503
});
484504
if (quotedPost == null) {
485-
throw new Error("Quoted post not found.");
505+
throw new InvalidInputError("quotedPostId");
486506
}
487507
}
488508
return await withTransaction(ctx.fedCtx, async (context) => {
@@ -511,7 +531,7 @@ builder.relayMutationField(
511531
{ replyTarget, quotedPost },
512532
);
513533
if (note == null) {
514-
throw new Error("Failed to create note.");
534+
throw new Error("Failed to create note");
515535
}
516536
return note;
517537
});

graphql/schema.graphql

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ input AccountLinkInput {
7878
url: URL!
7979
}
8080

81+
type AccountNotFoundError {
82+
query: String!
83+
}
84+
8185
type AccountNotificationsConnection {
8286
edges: [AccountNotificationsConnectionEdge!]!
8387
pageInfo: PageInfo!
@@ -284,6 +288,8 @@ type CreateNotePayload {
284288
note: Note!
285289
}
286290

291+
union CreateNoteResult = CreateNotePayload | InvalidInputError | NotAuthenticatedError
292+
287293
type CustomEmoji implements Node {
288294
id: ID!
289295
imageUrl: String!
@@ -360,6 +366,10 @@ type Instance implements Node {
360366
updated: DateTime!
361367
}
362368

369+
type InvalidInputError {
370+
inputPath: String!
371+
}
372+
363373
"""
364374
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
365375
"""
@@ -375,6 +385,8 @@ type LoginChallenge {
375385
token: UUID!
376386
}
377387

388+
union LoginResult = AccountNotFoundError | LoginChallenge
389+
378390
"""A Hackers' Pub-flavored Markdown text."""
379391
scalar Markdown
380392

@@ -409,7 +421,7 @@ type Mutation {
409421
"""The signup token."""
410422
token: UUID!
411423
): SignupResult!
412-
createNote(input: CreateNoteInput!): CreateNotePayload!
424+
createNote(input: CreateNoteInput!): CreateNoteResult!
413425
loginByEmail(
414426
"""The email of the account to sign in."""
415427
email: String!
@@ -421,7 +433,7 @@ type Mutation {
421433
The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`.
422434
"""
423435
verifyUrl: URITemplate!
424-
): LoginChallenge
436+
): LoginResult!
425437
loginByUsername(
426438
"""The locale for the sign-in email."""
427439
locale: Locale!
@@ -433,7 +445,7 @@ type Mutation {
433445
The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`.
434446
"""
435447
verifyUrl: URITemplate!
436-
): LoginChallenge
448+
): LoginResult!
437449

438450
"""Revoke a session by its ID."""
439451
revokeSession(
@@ -447,6 +459,10 @@ interface Node {
447459
id: ID!
448460
}
449461

462+
type NotAuthenticatedError {
463+
notAuthenticated: String!
464+
}
465+
450466
type Note implements Node & Post & Reactable {
451467
actor: Actor!
452468
content: HTML!

graphql/session.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import type { Session } from "@hackerspub/models/session";
22
import { Account } from "./account.ts";
33
import { builder } from "./builder.ts";
44

5+
export class NotAuthenticatedError extends Error {
6+
public constructor() {
7+
super("Not authenticated");
8+
}
9+
}
10+
11+
builder.objectType(NotAuthenticatedError, {
12+
name: "NotAuthenticatedError",
13+
fields: (t) => ({
14+
notAuthenticated: t.string({
15+
resolve: () => "",
16+
}),
17+
}),
18+
});
19+
520
export const SessionRef = builder.objectRef<Session>("Session");
621

722
SessionRef.implement({

0 commit comments

Comments
 (0)