Skip to content

Add reportPost GraphQL mutation for flag system#236

Draft
malkoG wants to merge 3 commits into
hackers-pub:mainfrom
malkoG:feature/flag-endpoint-with-minimum-requirement
Draft

Add reportPost GraphQL mutation for flag system#236
malkoG wants to merge 3 commits into
hackers-pub:mainfrom
malkoG:feature/flag-endpoint-with-minimum-requirement

Conversation

@malkoG

@malkoG malkoG commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements phase 1 of the flag/report system (#192) — a reportPost GraphQL mutation that allows authenticated users to report posts with duplicate prevention.

This is a minimal implementation to satisfy app store verification requirements (reporting/flagging capability is required for user-generated content apps). Further phases will add actor reporting, federation via ActivityPub Flag activity, and moderation workflows.

  • Adds flag database table with support for both post and actor reporting (actor reporting reserved for future use), unique constraints to prevent duplicate reports, and a check constraint ensuring exactly one target type
  • Adds createFlag model function with onConflictDoNothing for safe duplicate handling
  • Adds reportPost relay mutation with AlreadyReportedError, InvalidInputError (post not found or self-report), and NotAuthenticatedError

Test plan

  • Run deno task migrate to apply the new migration
  • Verify reportPost mutation works via GraphQL playground
  • Verify duplicate report returns AlreadyReportedError
  • Verify reporting own post returns InvalidInputError
  • Verify unauthenticated request returns NotAuthenticatedError

Implements the database layer for issue hackers-pub#192 (flag/report system).
Schema supports both post and actor reporting with duplicate
prevention via unique constraints.
@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@malkoG has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 40 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 40 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 507138c2-d696-4cfd-b055-7157d87557fc

📥 Commits

Reviewing files that changed from the base of the PR and between b59b8db and 0298f61.

📒 Files selected for processing (5)
  • graphql/mod.ts
  • graphql/report.ts
  • graphql/schema.graphql
  • models/flag.ts
  • models/relations.ts
📝 Walkthrough

Walkthrough

A new reporting feature: database table flag plus schema/relations and a models API, and a GraphQL Relay mutation reportPost that creates flags, prevents duplicates and self-reports, and returns the reported post.

Changes

Cohort / File(s) Summary
Database Migration
drizzle/0088_flag.sql
Adds public.flag table with UUID PK, iri, reporter_id, nullable post_id/actor_id, reason, created; unique constraints, check enforcing exactly one target, and FK constraints with ON DELETE CASCADE.
Schema & Relations
models/schema.ts, models/relations.ts
Introduces flagTable schema and inferred types (Flag, NewFlag); adds relations from actorTable and postTable to flagTable and relation entries for reporter/post/flaggedActor.
Models Export & Implementation
models/deno.json, models/flag.ts
Exports ./flag subpath; adds createFlag implementation returning { created, flagId }, using insert with onConflictDoNothing and fallback lookup on conflict.
GraphQL API
graphql/report.ts, graphql/mod.ts, graphql/schema.graphql
Registers reportPost Relay mutation and AlreadyReportedError; validates auth, post existence, prevents self-reporting, builds flag IRI, calls createFlag, errors on duplicates, and exposes post in payload.

Sequence Diagram

sequenceDiagram
    participant User as "User"
    participant GraphQL as "GraphQL Resolver"
    participant Models as "Flag Model"
    participant DB as "Database"

    User->>GraphQL: reportPost(postId, reason)
    GraphQL->>GraphQL: require ctx.account
    alt Not authenticated
        GraphQL-->>User: NotAuthenticatedError
    else
        GraphQL->>DB: SELECT post (by id, actorId)
        alt Post missing
            GraphQL-->>User: InvalidInputError("postId")
        else
            alt reporter is post author
                GraphQL-->>User: InvalidInputError("postId")
            else
                GraphQL->>Models: createFlag(iri, reporterId, postId, reason)
                Models->>DB: INSERT INTO flag ON CONFLICT DO NOTHING
                alt Insert succeeded
                    Models-->>GraphQL: { created: true, flagId }
                    GraphQL->>DB: SELECT post (for payload)
                    GraphQL-->>User: ReportPostPayload { post }
                else
                    Models->>DB: SELECT id FROM flag WHERE reporter_id=... AND post_id=...
                    Models-->>GraphQL: { created: false, flagId }
                    GraphQL-->>User: AlreadyReportedError
                end
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • dahlia

Poem

🐰 I hopped a quick report, an IRI to send,
A reason tucked in carrot-mail to mend.
No double hops allowed, one flag per find,
The burrow keeps order — gentle, swift, and kind. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: adding a reportPost GraphQL mutation for the flag system.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@malkoG malkoG marked this pull request as ready for review April 8, 2026 05:53
@malkoG

malkoG commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a post reporting feature, adding a flag table to the database and a reportPost mutation to the GraphQL API. The implementation includes logic to prevent duplicate reports and handle authentication. Feedback highlights a critical ID mismatch between the generated IRI and the database record, suggesting that the UUID be passed explicitly to the creation function. There are also recommendations to use explicit aliases in the table relations to resolve ambiguity between the reporter and the flagged actor.

Comment thread models/flag.ts
Comment on lines +10 to +21
export async function createFlag(
db: Database,
iri: string,
reporterId: Uuid,
postId: Uuid,
reason: string,
): Promise<CreateFlagResult> {
const id = generateUuidV7();
const rows = await db.insert(flagTable)
.values({ id, iri, reporterId, postId, reason })
.onConflictDoNothing()
.returning({ id: flagTable.id });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The createFlag function generates a new UUID for the id field, but the iri passed by the caller already contains a UUID (generated in reportPost). This results in a mismatch between the flag's primary key and the ID referenced in its IRI. The function should accept the id as an argument to ensure consistency.

Suggested change
export async function createFlag(
db: Database,
iri: string,
reporterId: Uuid,
postId: Uuid,
reason: string,
): Promise<CreateFlagResult> {
const id = generateUuidV7();
const rows = await db.insert(flagTable)
.values({ id, iri, reporterId, postId, reason })
.onConflictDoNothing()
.returning({ id: flagTable.id });
export async function createFlag(
db: Database,
id: Uuid,
iri: string,
reporterId: Uuid,
postId: Uuid,
reason: string,
): Promise<CreateFlagResult> {
const rows = await db.insert(flagTable)
.values({ id, iri, reporterId, postId, reason })
.onConflictDoNothing()
.returning({ id: flagTable.id });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — createFlag now accepts id as a parameter so it matches the IRI.

Comment thread graphql/report.ts
Comment on lines +62 to +68
const result = await createFlag(
ctx.db,
iri,
ctx.account.actor.id,
post.id,
args.input.reason,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Pass the generated flagId to createFlag to ensure the database record's ID matches the one used in the IRI.

Suggested change
const result = await createFlag(
ctx.db,
iri,
ctx.account.actor.id,
post.id,
args.input.reason,
);
const result = await createFlag(
ctx.db,
flagId,
iri,
ctx.account.actor.id,
post.id,
args.input.reason,
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — flagId is now passed through to createFlag.

Comment thread models/relations.ts Outdated
posts: r.many.postTable(),
pins: r.many.pinTable(),
votedPolls: r.many.pollTable(),
flags: r.many.flagTable(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The flags relation in actorTable is ambiguous because flagTable has two relations to actorTable (reporter and flaggedActor). You should disambiguate them using aliases. For example, use reportedFlags with the reporter alias.

Suggested change
flags: r.many.flagTable(),
reportedFlags: r.many.flagTable({ alias: "reporter" }),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — split into reportedFlags (alias reporter) and receivedFlags (alias flaggedActor) on actorTable, matching the blockingTable pattern.

Comment thread models/relations.ts
Comment on lines +378 to +382
reporter: r.one.actorTable({
from: r.flagTable.reporterId,
to: r.actorTable.id,
optional: false,
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Add an explicit alias to the reporter relation to allow disambiguation in the actorTable relations.

    reporter: r.one.actorTable({
      alias: "reporter",
      from: r.flagTable.reporterId,
      to: r.actorTable.id,
      optional: false,
    }),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — added alias: "reporter" to the reporter relation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
graphql/report.ts (1)

77-89: Minor: Non-null assertion on refetched post.

Line 85 uses post! which could fail if the post is deleted between mutation resolution and output field resolution. This is a rare race condition but could cause an unhandled error.

Consider returning null and making the post field nullable, or handling the edge case explicitly.

Optional: Handle deleted post edge case
       post: t.drizzleField({
-        type: Post,
+        type: Post,
+        nullable: true,
         async resolve(query, result, _args, ctx) {
           const post = await ctx.db.query.postTable.findFirst(
             query({ where: { id: result.postId } }),
           );
-          return post!;
+          return post ?? null;
         },
       }),

Note: This would require updating ReportPostPayload.post to be nullable in the schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/report.ts` around lines 77 - 89, The output field resolver for post
in outputFields uses a non-null assertion (post!) which can throw if the record
was deleted between mutation and resolution; update the resolver in outputFields
-> post (the async resolve function) to return post || null instead of
asserting, and update the GraphQL type for ReportPostPayload.post to be nullable
(or else handle the missing post by throwing a controlled error), ensuring
callers and schema reflect the nullable post.
models/flag.ts (2)

10-21: Consider accepting id as a parameter for IRI consistency.

The function generates its own UUID for id (line 17), but the caller in graphql/report.ts generates a separate flagId for the IRI path. This means the flag's actual id differs from the UUID embedded in its iri.

While this works, it's unusual for the IRI to reference a different UUID than the record's primary key. Consider accepting id as a parameter to ensure consistency:

Optional: Accept id as parameter
 export async function createFlag(
   db: Database,
+  id: Uuid,
   iri: string,
   reporterId: Uuid,
   postId: Uuid,
   reason: string,
 ): Promise<CreateFlagResult> {
-  const id = generateUuidV7();
   const rows = await db.insert(flagTable)

Then in graphql/report.ts:

-      const result = await createFlag(
-        ctx.db,
-        iri,
+      const result = await createFlag(
+        ctx.db,
+        flagId,
+        iri,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/flag.ts` around lines 10 - 21, The createFlag function currently calls
generateUuidV7() internally so the stored primary key can differ from the UUID
embedded in the IRI; change createFlag signature to accept an id parameter
(e.g., id: Uuid) instead of generating one, update all callers to pass the same
flagId they use for building the IRI (the caller that currently creates flagId
should pass it into createFlag), and remove generateUuidV7() usage inside
createFlag so flagTable.insert uses the provided id to ensure the DB id matches
the IRI UUID.

27-32: Non-null assertion may cause runtime error on edge cases.

If onConflictDoNothing() is triggered by a conflict on the iri unique constraint (rather than the (reporterId, postId) constraint), the subsequent findFirst query by (reporterId, postId) will return null, causing a runtime crash at existing!.id.

While IRI collisions are extremely unlikely with UUIDv7, consider adding defensive handling:

Proposed defensive handling
   const existing = await db.query.flagTable.findFirst({
     columns: { id: true },
     where: { reporterId, postId },
   });

-  return { created: false, flagId: existing!.id };
+  if (existing == null) {
+    throw new Error("Flag conflict detected but no existing flag found");
+  }
+  return { created: false, flagId: existing.id };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/flag.ts` around lines 27 - 32, The code assumes existing is non-null
after onConflictDoNothing() and uses existing!.id, which can crash if the
conflict was on the iri unique constraint; update the logic around
db.query.flagTable.findFirst to defensively handle a null result: after the
conflict, check if existing is null and if so re-query the flag table by the iri
unique key (or run a broader lookup) to locate the record, and only then return
{ created: false, flagId: ... }; if no record is found return { created: false,
flagId: null } instead of using a non-null assertion. Ensure you update the code
paths that reference existing (the existing variable and the return) so they
handle null safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@models/relations.ts`:
- Line 78: The flags relation is ambiguous because flagTable has two foreign
keys to actorTable (reporterId and actorId); update the relation definition for
flags (currently `flags: r.many.flagTable()`) to disambiguate by adding an alias
so Drizzle knows to use the reporterId FK — e.g., change the relation to use `{
alias: "reporter" }` on the flagTable relation to match the pattern used for
followingTable/blockingTable.

---

Nitpick comments:
In `@graphql/report.ts`:
- Around line 77-89: The output field resolver for post in outputFields uses a
non-null assertion (post!) which can throw if the record was deleted between
mutation and resolution; update the resolver in outputFields -> post (the async
resolve function) to return post || null instead of asserting, and update the
GraphQL type for ReportPostPayload.post to be nullable (or else handle the
missing post by throwing a controlled error), ensuring callers and schema
reflect the nullable post.

In `@models/flag.ts`:
- Around line 10-21: The createFlag function currently calls generateUuidV7()
internally so the stored primary key can differ from the UUID embedded in the
IRI; change createFlag signature to accept an id parameter (e.g., id: Uuid)
instead of generating one, update all callers to pass the same flagId they use
for building the IRI (the caller that currently creates flagId should pass it
into createFlag), and remove generateUuidV7() usage inside createFlag so
flagTable.insert uses the provided id to ensure the DB id matches the IRI UUID.
- Around line 27-32: The code assumes existing is non-null after
onConflictDoNothing() and uses existing!.id, which can crash if the conflict was
on the iri unique constraint; update the logic around
db.query.flagTable.findFirst to defensively handle a null result: after the
conflict, check if existing is null and if so re-query the flag table by the iri
unique key (or run a broader lookup) to locate the record, and only then return
{ created: false, flagId: ... }; if no record is found return { created: false,
flagId: null } instead of using a non-null assertion. Ensure you update the code
paths that reference existing (the existing variable and the return) so they
handle null safely.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 750953b4-bd20-47c1-bbff-4d6166aeee7f

📥 Commits

Reviewing files that changed from the base of the PR and between a605849 and b59b8db.

📒 Files selected for processing (8)
  • drizzle/0088_flag.sql
  • graphql/mod.ts
  • graphql/report.ts
  • graphql/schema.graphql
  • models/deno.json
  • models/flag.ts
  • models/relations.ts
  • models/schema.ts

Comment thread models/relations.ts Outdated
Registers the reportPost relay mutation with duplicate
detection (AlreadyReportedError) and self-report prevention.
@malkoG malkoG force-pushed the feature/flag-endpoint-with-minimum-requirement branch from b59b8db to 3e30387 Compare April 8, 2026 06:04
Add explicit aliases to flagTable actor relations
to match the blockingTable pattern, avoiding ambiguity
from two FKs to actorTable.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
models/relations.ts (1)

78-78: ⚠️ Potential issue | 🟡 Minor

Add alias to disambiguate the foreign key relation.

The flagTable has two foreign keys to actorTable: reporterId (the reporter) and actorId (the flagged actor). Without an alias, Drizzle cannot determine which FK the r.many.flagTable() relation should use. Follow the pattern used for followingTable and blockingTable relations.

🔧 Proposed fix
-    flags: r.many.flagTable(),
+    flags: r.many.flagTable({ alias: "reporter" }),

And update the flagTable reporter relation to include the matching alias:

     reporter: r.one.actorTable({
+      alias: "reporter",
       from: r.flagTable.reporterId,
       to: r.actorTable.id,
       optional: false,
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/relations.ts` at line 78, Add an explicit alias to the actor -> flags
relation so Drizzle can pick the correct foreign key: change the relation call
r.many.flagTable() to include an alias option (matching the pattern used by
followingTable and blockingTable), and then update the corresponding reporter
relation definition on flagTable to use the same alias so the reporterId FK is
unambiguous.
🧹 Nitpick comments (4)
drizzle/0088_flag.sql (1)

1-17: Consider adding indexes for query performance.

The table has unique constraints that will create indexes for (reporter_id, post_id) and (reporter_id, actor_id), but if you need to query flags by post_id alone (e.g., listing all reports for a post in a moderation dashboard), an additional index on post_id would improve performance.

🔧 Optional index addition
 ALTER TABLE "flag" ADD CONSTRAINT "flag_actor_id_actor_id_fk" FOREIGN KEY ("actor_id") REFERENCES "public"."actor"("id") ON DELETE cascade ON UPDATE no action;
+--> statement-breakpoint
+CREATE INDEX "idx_flag_post_id" ON "flag" ("post_id");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle/0088_flag.sql` around lines 1 - 17, Add an index on the "flag" table
for the post_id column to speed queries filtering by post (e.g., create an index
on "flag"(post_id)); locate the table/constraints related to "flag" (CONSTRAINT
"flag_post_id_post_id_fk", columns "post_id") and add a non-unique index for
post_id; optionally also add a separate index on "actor_id" if you expect
frequent lookups by actor.
graphql/report.ts (2)

48-54: Consider distinguishing "post not found" from "self-report" errors.

Both cases throw InvalidInputError("postId"), which makes it impossible for clients to distinguish between a non-existent post and attempting to report one's own post. This could hinder user feedback in the UI.

💡 Alternative approach

You could either:

  1. Use different inputPath values: InvalidInputError("postId") vs InvalidInputError("postId.self")
  2. Create a dedicated error type for self-reporting (e.g., CannotReportOwnPostError)

Option 1 (minimal change):

       if (post.actorId === ctx.account.actor.id) {
-        throw new InvalidInputError("postId");
+        throw new InvalidInputError("postId.selfReport");
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/report.ts` around lines 48 - 54, The two error cases in
graphql/report.ts both throw InvalidInputError("postId"), which prevents clients
from distinguishing a missing post from a self-report; update the self-report
branch (the check post.actorId === ctx.account.actor.id) to return a distinct
error—either throw InvalidInputError("postId.self") or a new error type (e.g.,
CannotReportOwnPostError) so callers can tell "post not found" (post == null ->
InvalidInputError("postId")) from "cannot report your own post" (post.actorId
=== ctx.account.actor.id -> distinct error).

81-85: Non-null assertion on post lookup in output field.

If the post is deleted between mutation resolution and output field resolution, post! will throw an unexpected error. While this is a narrow race window, adding defensive handling would be more robust.

🛡️ Defensive handling suggestion
         async resolve(query, result, _args, ctx) {
           const post = await ctx.db.query.postTable.findFirst(
             query({ where: { id: result.postId } }),
           );
-          return post!;
+          if (post == null) {
+            throw new Error("Post was deleted");
+          }
+          return post;
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@graphql/report.ts` around lines 81 - 85, The resolver in resolve
(graphql/report.ts) currently uses a non-null assertion on the post lookup
("post!") which can throw if the post was deleted; change it to handle a null
result from ctx.db.query.postTable.findFirst({ where: { id: result.postId } })
by either returning null (or an appropriate empty value) or throwing a
controlled GraphQL error (e.g., new Error or ApolloError) so resolution is
deterministic; update the resolve return behavior/type accordingly and ensure
callers/typedefs accept the nullable case instead of using the non-null
assertion.
models/flag.ts (1)

27-32: Non-null assertion could mask unexpected errors.

The existing!.id assertion on line 32 assumes that if the insert failed due to conflict, the existing record must be found. While this is true for the (reporter_id, post_id) unique constraint, the onConflictDoNothing() also covers the iri unique constraint. Although IRI collision is practically impossible with UUIDv7, adding defensive handling would make the code more robust.

🛡️ Defensive handling suggestion
   const existing = await db.query.flagTable.findFirst({
     columns: { id: true },
     where: { reporterId, postId },
   });

-  return { created: false, flagId: existing!.id };
+  if (existing == null) {
+    throw new Error("Flag conflict but no existing record found");
+  }
+  return { created: false, flagId: existing.id };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/flag.ts` around lines 27 - 32, The code currently uses a non-null
assertion existing!.id after calling db.query.flagTable.findFirst; replace this
with a safe null-check: call db.query.flagTable.findFirst({ columns: { id: true
}, where: { reporterId, postId } }) and if existing is null, handle it
defensively (e.g., attempt a secondary lookup by iri or throw a descriptive
Error/log and return created: false with flagId: null) instead of asserting;
avoid existing! and ensure any onConflictDoNothing() path that left no matching
row is detected and reported using identifiers reporterId, postId, and iri.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@models/relations.ts`:
- Line 78: Add an explicit alias to the actor -> flags relation so Drizzle can
pick the correct foreign key: change the relation call r.many.flagTable() to
include an alias option (matching the pattern used by followingTable and
blockingTable), and then update the corresponding reporter relation definition
on flagTable to use the same alias so the reporterId FK is unambiguous.

---

Nitpick comments:
In `@drizzle/0088_flag.sql`:
- Around line 1-17: Add an index on the "flag" table for the post_id column to
speed queries filtering by post (e.g., create an index on "flag"(post_id));
locate the table/constraints related to "flag" (CONSTRAINT
"flag_post_id_post_id_fk", columns "post_id") and add a non-unique index for
post_id; optionally also add a separate index on "actor_id" if you expect
frequent lookups by actor.

In `@graphql/report.ts`:
- Around line 48-54: The two error cases in graphql/report.ts both throw
InvalidInputError("postId"), which prevents clients from distinguishing a
missing post from a self-report; update the self-report branch (the check
post.actorId === ctx.account.actor.id) to return a distinct error—either throw
InvalidInputError("postId.self") or a new error type (e.g.,
CannotReportOwnPostError) so callers can tell "post not found" (post == null ->
InvalidInputError("postId")) from "cannot report your own post" (post.actorId
=== ctx.account.actor.id -> distinct error).
- Around line 81-85: The resolver in resolve (graphql/report.ts) currently uses
a non-null assertion on the post lookup ("post!") which can throw if the post
was deleted; change it to handle a null result from
ctx.db.query.postTable.findFirst({ where: { id: result.postId } }) by either
returning null (or an appropriate empty value) or throwing a controlled GraphQL
error (e.g., new Error or ApolloError) so resolution is deterministic; update
the resolve return behavior/type accordingly and ensure callers/typedefs accept
the nullable case instead of using the non-null assertion.

In `@models/flag.ts`:
- Around line 27-32: The code currently uses a non-null assertion existing!.id
after calling db.query.flagTable.findFirst; replace this with a safe null-check:
call db.query.flagTable.findFirst({ columns: { id: true }, where: { reporterId,
postId } }) and if existing is null, handle it defensively (e.g., attempt a
secondary lookup by iri or throw a descriptive Error/log and return created:
false with flagId: null) instead of asserting; avoid existing! and ensure any
onConflictDoNothing() path that left no matching row is detected and reported
using identifiers reporterId, postId, and iri.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60034097-018c-46de-a3a3-4446a83eb808

📥 Commits

Reviewing files that changed from the base of the PR and between a605849 and b59b8db.

📒 Files selected for processing (8)
  • drizzle/0088_flag.sql
  • graphql/mod.ts
  • graphql/report.ts
  • graphql/schema.graphql
  • models/deno.json
  • models/flag.ts
  • models/relations.ts
  • models/schema.ts

@malkoG malkoG marked this pull request as draft April 8, 2026 21:09
@dahlia dahlia force-pushed the main branch 2 times, most recently from ff82b29 to 38a6e99 Compare May 5, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant