Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@pothos/core": "npm:@pothos/core@^4.7.3",
"@pothos/plugin-complexity": "npm:@pothos/[email protected]",
"@pothos/plugin-drizzle": "npm:@pothos/plugin-drizzle@^0.11.1",
"@pothos/plugin-errors": "npm:@pothos/plugin-errors@^4.4.2",
"@pothos/plugin-relay": "npm:@pothos/plugin-relay@^4.6.2",
Comment on lines +45 to 46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

New errors plugin mapping added — verify version compatibility and standardize semver usage

Good addition. Two quick checks:

  • Compatibility: Ensure @pothos/plugin-errors@^4.4.2 declares peer compatibility with @pothos/core@^4.7.3 you use.
  • Consistency: Your Pothos plugins mix exact and caret ranges (e.g., @pothos/[email protected] vs others with ^). Consider standardizing to reduce accidental partial upgrades.

You can sanity-check peer ranges from npm without cloning:


🏁 Script executed:

#!/bin/bash
# Check peer deps for @pothos/plugin-errors and @pothos/core versions
set -euo pipefail
curl -s https://registry.npmjs.org/@pothos/plugin-errors | jq -r '
  .["dist-tags"].latest as $latest
  | "latest: " + $latest,
    "peerDependencies:",
    (.versions[$latest].peerDependencies // {})
'
curl -s https://registry.npmjs.org/@pothos/core | jq -r '."dist-tags".latest as $latest | "core latest: " + $latest'

Length of output: 445


Verify peer compatibility and unify semver ranges in deno.json

  • @pothos/plugin-errors@^4.4.2 declares @pothos/core: "*" so it’s compatible with your core version.
  • Your other plugins mix exact and caret ranges (e.g. @pothos/[email protected] vs. ^4.x). Pick one style to avoid surprises.

Files to update:

  • deno.json

Suggested change:

   "@pothos/plugin-errors": "npm:@pothos/plugin-errors@^4.4.2",
   "@pothos/plugin-relay": "npm:@pothos/plugin-relay@^4.6.2",
-  "@pothos/plugin-complexity": "npm:@pothos/[email protected]",
+  "@pothos/plugin-complexity": "npm:@pothos/plugin-complexity@^4.1.1",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@pothos/plugin-errors": "npm:@pothos/plugin-errors@^4.4.2",
"@pothos/plugin-relay": "npm:@pothos/plugin-relay@^4.6.2",
"@pothos/plugin-errors": "npm:@pothos/plugin-errors@^4.4.2",
"@pothos/plugin-relay": "npm:@pothos/plugin-relay@^4.6.2",
"@pothos/plugin-complexity": "npm:@pothos/plugin-complexity@^4.1.1",
🤖 Prompt for AI Agents
In deno.json around lines 45 to 46, the version specifications for @pothos
plugins mix exact and caret semver ranges, which can cause inconsistencies.
Verify that all @pothos plugins use compatible peer dependencies, then unify the
version ranges by choosing either caret (^) or exact versions consistently
across all @pothos plugin entries to ensure predictable dependency resolution.

"@pothos/plugin-scope-auth": "npm:@pothos/plugin-scope-auth@^4.1.5",
"@pothos/plugin-simple-objects": "npm:@pothos/plugin-simple-objects@^4.1.3",
Expand Down
9 changes: 9 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions graphql/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Uuid } from "@hackerspub/models/uuid";
import SchemaBuilder from "@pothos/core";
import ComplexityPlugin from "@pothos/plugin-complexity";
import DrizzlePlugin from "@pothos/plugin-drizzle";
import ErrorsPlugin from "@pothos/plugin-errors";
import RelayPlugin from "@pothos/plugin-relay";
import ScopeAuthPlugin from "@pothos/plugin-scope-auth";
import SimpleObjectsPlugin from "@pothos/plugin-simple-objects";
Expand All @@ -29,6 +30,9 @@ import {
import { createGraphQLError } from "graphql-yoga";
import type Keyv from "keyv";

export type ValuesOfEnumType<T> = T extends
PothosSchemaTypes.EnumRef<never, unknown, infer V> ? V : never;

export interface ServerContext {
db: Database;
kv: Keyv;
Expand Down Expand Up @@ -114,6 +118,7 @@ export const builder = new SchemaBuilder<PothosTypes>({
SimpleObjectsPlugin,
TracingPlugin,
WithInputPlugin,
ErrorsPlugin,
],
complexity: {
defaultComplexity: 1,
Expand Down Expand Up @@ -148,6 +153,16 @@ export const builder = new SchemaBuilder<PothosTypes>({
relay: {
clientMutationId: "optional",
},
errors: {
directResult: true,
defaultUnionOptions: {
name(options) {
return `${options.fieldName.charAt(0).toUpperCase()}${
options.fieldName.slice(1)
}Result`;
},
},
},
});

builder.addScalarType("Date", DateResolver);
Expand Down
43 changes: 38 additions & 5 deletions graphql/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,24 @@ import { sql } from "drizzle-orm";
import { parseTemplate } from "url-template";
import { Account } from "./account.ts";
import { builder } from "./builder.ts";
import { SessionRef } from "./session.ts";
import { EMAIL_FROM } from "./email.ts";
import { SessionRef } from "./session.ts";

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

class AccountNotFoundError extends Error {
public constructor(public readonly query: string) {
super(`Account not found`);
}
}

builder.objectType(AccountNotFoundError, {
name: "AccountNotFoundError",
fields: (t) => ({
query: t.exposeString("query"),
}),
});

interface LoginChallenge {
accountId: Uuid;
token: Uuid;
Expand Down Expand Up @@ -54,7 +67,15 @@ LoginChallengeRef.implement({
builder.mutationFields((t) => ({
loginByUsername: t.field({
type: LoginChallengeRef,
nullable: true,
errors: {
types: [AccountNotFoundError],
union: {
name: "LoginResult",
},
result: {
name: "LoginSuccess",
},
},
args: {
username: t.arg.string({
required: true,
Expand All @@ -80,7 +101,9 @@ builder.mutationFields((t) => ({
with: { emails: true },
where: { username: args.username },
});
if (account == null) return null;
if (account == null) {
throw new AccountNotFoundError(args.username);
}
const token = await createSigninToken(ctx.kv, account.id);
const messages: Message[] = [];
for (const { email } of account.emails) {
Expand Down Expand Up @@ -110,7 +133,15 @@ builder.mutationFields((t) => ({

loginByEmail: t.field({
type: LoginChallengeRef,
nullable: true,
errors: {
types: [AccountNotFoundError],
union: {
name: "LoginResult",
},
result: {
name: "LoginSuccess",
},
},
args: {
email: t.arg.string({
required: true,
Expand Down Expand Up @@ -150,7 +181,9 @@ builder.mutationFields((t) => ({
with: { emails: true },
});
}
if (account == null) return null;
if (account == null) {
throw new AccountNotFoundError(args.email);
}
const token = await createSigninToken(ctx.kv, account.id);
const messages: Message[] = [];
for (const { email } of account.emails) {
Expand Down
28 changes: 24 additions & 4 deletions graphql/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Account } from "./account.ts";
import { Actor } from "./actor.ts";
import { builder, Node } from "./builder.ts";
import { Reactable } from "./reactable.ts";
import { NotAuthenticatedError } from "./session.ts";

const PostVisibility = builder.enumType("PostVisibility", {
values: [
Expand All @@ -22,6 +23,19 @@ const PostVisibility = builder.enumType("PostVisibility", {
] as const,
});

class InvalidInputError extends Error {
public constructor(public readonly inputPath: string) {
super(`Invalid input - ${inputPath}`);
}
}

builder.objectType(InvalidInputError, {
name: "InvalidInputError",
fields: (t) => ({
inputPath: t.expose("inputPath", { type: "String" }),
}),
});

export const Post = builder.drizzleInterface("postTable", {
variant: "Post",
interfaces: [Reactable, Node],
Expand Down Expand Up @@ -458,10 +472,16 @@ builder.relayMutationField(
}),
},
{
errors: {
types: [
NotAuthenticatedError,
InvalidInputError,
],
},
async resolve(_root, args, ctx) {
const session = await ctx.session;
if (session == null) {
throw new Error("Not authenticated.");
throw new NotAuthenticatedError();
}
const { visibility, content, language, replyTargetId, quotedPostId } =
args.input;
Expand All @@ -472,7 +492,7 @@ builder.relayMutationField(
where: { id: replyTargetId.id },
});
if (replyTarget == null) {
throw new Error("Reply target not found.");
throw new InvalidInputError("replyTargetId");
}
}
let quotedPost: schema.Post & { actor: schema.Actor } | undefined;
Expand All @@ -482,7 +502,7 @@ builder.relayMutationField(
where: { id: quotedPostId.id },
});
if (quotedPost == null) {
throw new Error("Quoted post not found.");
throw new InvalidInputError("quotedPostId");
}
}
return await withTransaction(ctx.fedCtx, async (context) => {
Expand Down Expand Up @@ -511,7 +531,7 @@ builder.relayMutationField(
{ replyTarget, quotedPost },
);
if (note == null) {
throw new Error("Failed to create note.");
throw new Error("Failed to create note");
}
return note;
});
Expand Down
22 changes: 19 additions & 3 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ input AccountLinkInput {
url: URL!
}

type AccountNotFoundError {
query: String!
}

type AccountNotificationsConnection {
edges: [AccountNotificationsConnectionEdge!]!
pageInfo: PageInfo!
Expand Down Expand Up @@ -284,6 +288,8 @@ type CreateNotePayload {
note: Note!
}

union CreateNoteResult = CreateNotePayload | InvalidInputError | NotAuthenticatedError

type CustomEmoji implements Node {
id: ID!
imageUrl: String!
Expand Down Expand Up @@ -360,6 +366,10 @@ type Instance implements Node {
updated: DateTime!
}

type InvalidInputError {
inputPath: String!
}

"""
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
"""
Expand All @@ -375,6 +385,8 @@ type LoginChallenge {
token: UUID!
}

union LoginResult = AccountNotFoundError | LoginChallenge

"""A Hackers' Pub-flavored Markdown text."""
scalar Markdown

Expand Down Expand Up @@ -409,7 +421,7 @@ type Mutation {
"""The signup token."""
token: UUID!
): SignupResult!
createNote(input: CreateNoteInput!): CreateNotePayload!
createNote(input: CreateNoteInput!): CreateNoteResult!
loginByEmail(
"""The email of the account to sign in."""
email: String!
Expand All @@ -421,7 +433,7 @@ type Mutation {
The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`.
"""
verifyUrl: URITemplate!
): LoginChallenge
): LoginResult!
loginByUsername(
"""The locale for the sign-in email."""
locale: Locale!
Expand All @@ -433,7 +445,7 @@ type Mutation {
The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`.
"""
verifyUrl: URITemplate!
): LoginChallenge
): LoginResult!

"""Revoke a session by its ID."""
revokeSession(
Expand All @@ -447,6 +459,10 @@ interface Node {
id: ID!
}

type NotAuthenticatedError {
notAuthenticated: String!
}

type Note implements Node & Post & Reactable {
actor: Actor!
content: HTML!
Expand Down
15 changes: 15 additions & 0 deletions graphql/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import type { Session } from "@hackerspub/models/session";
import { Account } from "./account.ts";
import { builder } from "./builder.ts";

export class NotAuthenticatedError extends Error {
public constructor() {
super("Not authenticated");
}
}

builder.objectType(NotAuthenticatedError, {
name: "NotAuthenticatedError",
fields: (t) => ({
notAuthenticated: t.string({
resolve: () => "",
}),
}),
});

export const SessionRef = builder.objectRef<Session>("Session");

SessionRef.implement({
Expand Down
Loading