Skip to content

Add actors table and actor GraphQL API - #47

Open
2chanhaeng wants to merge 14 commits into
fedify-dev:mainfrom
2chanhaeng:create-actor
Open

Add actors table and actor GraphQL API#47
2chanhaeng wants to merge 14 commits into
fedify-dev:mainfrom
2chanhaeng:create-actor

Conversation

@2chanhaeng

@2chanhaeng 2chanhaeng commented Aug 4, 2026

Copy link
Copy Markdown
Member

Resolves #6.
Add actors table and actor GraphQL API to create and reads.

To merge this PR, #44 must be preceded.

Before starting the server, please remove the existing .pgdata/ to clean up the DB.

Assisted-by: Codex:gpt-5-6-sol to generate test and implement merging remoteActors into actors.
Assisted-by: Claude Code:claude-fable-5 to plan merging remoteActors into actors.

@2chanhaeng
2chanhaeng requested review from dahlia, dodok8 and sij411 August 4, 2026 05:14
@2chanhaeng 2chanhaeng self-assigned this Aug 4, 2026
dodok8
dodok8 previously approved these changes Aug 13, 2026
@sij411

sij411 commented Aug 14, 2026

Copy link
Copy Markdown
Member

I have some comments related to actor schema. I see only followee/follower counters not actual follow relationship among actors. I don't think remoteActors.followeeUrl is the right method to fetch remote actor's following/followers. It's 'remote' that means their server status is outside our controls. So we should think about storing follow relationship i think. Also as far as i know, 'following' is more common than the word 'followee' in other ActivityPub implementations.

@2chanhaeng

Copy link
Copy Markdown
Member Author

I have some comments related to actor schema. I see only followee/follower counters not actual follow relationship among actors. I don't think remoteActors.followeeUrl is the right method to fetch remote actor's following/followers. It's 'remote' that means their server status is outside our controls. So we should think about storing follow relationship i think. Also as far as i know, 'following' is more common than the word 'followee' in other ActivityPub implementations.

I haven't fully implemented the follow-related features yet since they weren't part of the current goal. I've kept the terminology as consistent as possible with the Hackers' Pub codebase. Personally, I also prefer the term "followee" because it provides a clear contrast to "follower."

Comment thread packages/graphql/src/actor.ts Outdated
@2chanhaeng
2chanhaeng requested review from dodok8 and sij411 August 21, 2026 08:28
host: "test-instance.drfed.org",
},
local: {
avatar: "avatar.png",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we don't need avatars and headers for users ? I don't think it's relevant to interop debugging

@sij411 sij411 Aug 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

may not activitypub.academy supports that. just wondering your thoughts

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sometimes profile image updates aren't being sent between instances. We need to test for that, I think?

Comment thread packages/graphql/src/instance.ts Outdated
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Aug 29, 2026
Comment thread packages/graphql/src/actor.ts Outdated
Comment thread packages/graphql/src/actor.ts Outdated
Comment thread packages/graphql/src/uri-templates.ts Outdated
@dahlia dahlia added this to the DrFed 0.1.0 milestone Sep 4, 2026
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
Apply the fixes from an AI-assisted code review of the create-actor
branch (fedify-dev#47).

 -  Mount the Fedify federation in front of the Yoga server.  The srvx
    fetch handler now calls federation.fetch() and falls back to Yoga
    via onNotFound and onNotAcceptable, so the actor, WebFinger, inbox,
    and collection dispatchers are reachable over HTTP instead of only
    serving as URI builders.
 -  Close the database client (PGlite or postgres.js) during shutdown
    before exiting the process.
 -  Serialize genActors per instance by locking the local_instances row
    with SELECT ... FOR UPDATE before counting actors.  This closes the
    race where two concurrent calls both passed the maxActors check
    under READ COMMITTED.  The post-insert recount, tx.rollback(), the
    outer mutable state, and the string-matched catch block are gone;
    every error path now returns a result object directly from the
    transaction callback.
 -  Derive the instance host from local_instances.slug and ctx.root
    rather than reading instances.host.
 -  Generate one UUID per actor and reuse it for local_actors.id,
    actors.id, actors.localId, and the placeholder username, dropping
    the .returning() round trip on the local_actors insert.
 -  Push the local-actor and host predicates into the Drizzle relational
    where clause in findLocalActor and mapHandle, replacing the full-row
    load plus JS filtering and the two-step instance/actor lookup.
 -  Import hashSecret from auth/hash.ts in actor.test.ts and
    instance.test.ts instead of keeping duplicate copies.

Provenance: the contributor asked Claude Code to review the branch
against the pull request comments and the overall diff.  Claude Code
produced the review findings (unmounted federation routes, the
maxActors race, the unclosed database client, the duplicated lookups,
and the duplicated test helper) and drafted this commit message.  The
contributor implemented every change in this commit by hand and
verified them with mise run check and the package test suites.

Assisted-by: Claude Code:claude-fable-5-1
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
2chanhaeng added a commit to 2chanhaeng/drfed that referenced this pull request Sep 4, 2026
Apply the fixes from an AI-assisted code review of the create-actor
branch (fedify-dev#47).

 -  Mount the Fedify federation in front of the Yoga server.  The srvx
    fetch handler now calls federation.fetch() and falls back to Yoga
    via onNotFound and onNotAcceptable, so the actor, WebFinger, inbox,
    and collection dispatchers are reachable over HTTP instead of only
    serving as URI builders.
 -  Close the database client (PGlite or postgres.js) during shutdown
    before exiting the process.
 -  Serialize genActors per instance by locking the local_instances row
    with SELECT ... FOR UPDATE before counting actors.  This closes the
    race where two concurrent calls both passed the maxActors check
    under READ COMMITTED.  The post-insert recount, tx.rollback(), the
    outer mutable state, and the string-matched catch block are gone;
    every error path now returns a result object directly from the
    transaction callback.
 -  Derive the instance host from local_instances.slug and ctx.root
    rather than reading instances.host.
 -  Generate one UUID per actor and reuse it for local_actors.id,
    actors.id, actors.localId, and the placeholder username, dropping
    the .returning() round trip on the local_actors insert.
 -  Push the local-actor and host predicates into the Drizzle relational
    where clause in findLocalActor and mapHandle, replacing the full-row
    load plus JS filtering and the two-step instance/actor lookup.
 -  Import hashSecret from auth/hash.ts in actor.test.ts and
    instance.test.ts instead of keeping duplicate copies.

Provenance: the contributor asked Claude Code to review the branch
against the pull request comments and the overall diff.  Claude Code
produced the review findings (unmounted federation routes, the
maxActors race, the unclosed database client, the duplicated lookups,
and the duplicated test helper) and drafted this commit message.  The
contributor implemented every change in this commit by hand and
verified them with mise run check and the package test suites.

Assisted-by: Claude Code:claude-fable-5-1
@sij411

sij411 commented Sep 5, 2026

Copy link
Copy Markdown
Member

Conflict should be resolved first

Prompts:
Actor 를 위한 GraphQL 빌더를 추가했습니다. @packages/graphql/src/actor.test.ts 에 이를 위한 테스트를 생성해주세요. 파일 추가 직후 `mise run fmt` 로 라이선스를 추가하세요. 최소한 다음과 같은 테스트가 필요합니다.
- 액터 생성
- 로컬 액터 조회
- 리모트 액터 조회

AI provenance: The contributor asked Codex to write tests for the new
Actor GraphQL builder in packages/graphql/src/actor.test.ts, covering at
least actor creation, local actor lookup, and remote actor lookup, and to
run `mise run fmt` right after adding the file so the license header is
inserted.  Codex generated the whole test file: it reuses the existing
withTestHarness helper to seed accounts, instances, and actors, and adds
a Mutation.genActors test that creates local actors plus Actor query
tests for a local actor and a remote actor.  While making the tests
pass, the genActors mutation query in actor.ts gained an inner join on
local_instances so that actors are only generated for local instances.
The human contributor reviewed the generated tests, ran the formatter,
and ran the test suite locally before committing.

Assisted-by: Codex:gpt-5.6-sol
Apply the same shape to actors that 09be000 applied to instances:

 -  Move the remote_actors columns (iri, inboxUrl, outboxUrl, and the
    optional followers/followees/featured/profile/avatar/header URLs)
    into actors and drop the remote_actors table.  iriUrl is renamed to
    iri and stays unique.
 -  Drop the location enum column and the (id, location) composite
    FK/CHECK pairs.  Local actors are identified by actors.localId,
    a nullable unique FK to local_actors.id with cascade delete, which
    inverts the previous local_actors.id -> actors.id dependency.
    local_actors keeps only id, avatar, and header.
 -  Replace the actor migration.  The old 20260803204536 migration
    created actors with the "location" type that the later 20260818
    migration drops, so it could not apply on a fresh database and it
    was never on main.  The new migration is regenerated from the
    current snapshot.
 -  Store local actor URLs at creation time.  genActors inserts
    local_actors first, then actors with the URLs expanded from the
    URI templates using instances.host, and joins local_instances via
    instances.localId.  avatarUrl and headerUrl start as NULL.
 -  Simplify the Actor GraphQL type to expose stored columns, keep
    handle as a field-level select on instance.host, and add uuid,
    created, and a nullable local: LocalActor relation.  Replace the
    CreateActors node with Actor in CreateActorsSuccess.actors.
 -  Fix the Instance.actors connection, whose resolveNode returned the
    instanceId instead of the actor.
 -  Update the actor tests for the new seeds, fields, and assertions.

AI provenance: Claude Code was asked to analyze the instance merge in
09be000 and write a plan applying the same approach to actors, covering
the schema, migration, GraphQL API, and tests; it found that the branch
no longer type-checked after the rebase and that the old actor migration
depended on a dropped enum type.  Codex implemented the changes from
that plan.  Claude Code then reviewed the diff against the plan, ran
tsc for each package, and ran the test suite.  The human contributor
has read and reviewed both the plan and the implementation, and directly
modified some of the designs. They validated the generated migration SQL
and performed local checks and tests.

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.6-sol
Replace the ad-hoc URI templates in the GraphQL package with Fedify's
Federation object as the single source of truth for ActivityPub URIs.

federation.ts now registers the actor, inbox, outbox, followers,
following, and featured dispatchers, deriving each URL from the
`Context` getters instead of string templates. The actor dispatcher
scopes lookups to the requesting host so a multi-tenant DrFed instance
never leaks one instance's actors under another's host, maps deleted
actors to `Tombstone` objects, and resolves WebFinger handles via
`mapHandle`. The inbox listener registers a catch-all logger so
incoming activities are visible instead of silently discarded, since
the data model has no follows/posts tables yet to persist them.

actor.ts's `genActor` and the `handle` field now go through a Fedify
`Context` created from `ServerContext.federation` instead of the old
`uri-templates.ts` module, which is removed along with the
`@fedify/uri-template` based templates.

Adds `@fedify/vocab` as a direct dependency for the ActivityPub
vocabulary classes used to build actor responses.

Provenance: the contributor asked Claude Code to implement
`buildFederation` in federation.ts using Fedify dispatcher setters
(actor, inbox, outbox, followers, following, featured) and to replace
the `uri-templates.ts`-based URI generation in actor.ts with Fedify's
`Context` URI getters, then remove uri-templates.ts. Claude Code
inspected the installed Fedify/vocab type declarations and the
project's Drizzle schema to draft the dispatcher implementations,
including the tombstone handling for deleted actors, the per-host
actor scoping, and the inbox catch-all logger, refined after a
follow-up automated review of the draft flagged missing inbox
visibility and 404-vs-Tombstone handling for deleted actors. The
contributor then asked Claude Code to hoist `findLocalActor` out of
`buildFederation` into a module-level function that takes the
database as an explicit argument, which was applied directly. The
contributor verified the result by running `mise run check`, the
package's `tsc --noEmit`, its test suite, and by exercising the
dispatchers against a temporary PGlite-backed Federation instance
(actor lookup, tombstone response for deleted actors, followers
collection, and cross-host isolation).

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Claude Code:claude-sonnet-5
PostgreSQL access was split between two drivers: Drizzle used pg
(node-postgres) while Fedify's PostgresKvStore used postgres
(postgres-js), so a --postgres-url server opened two separate
connection pools.  Unify on postgres:

- @drfed/models: migrate() now uses drizzle-orm/postgres-js.  The
  PostgreSQL credentials mirror the PGlite shape: either a url with
  optional postgres.Options (a max: 1 client is created and closed
  after migrating) or an existing Sql client that is left open.
- @drfed/drfed: the --postgres-url parser creates one postgres()
  client and shares it between Drizzle and PostgresKvStore through
  credentials.client.
- Drop pg and @types/pg from both packages and the catalog; move
  postgres into the catalog.
- Prune the orphaned pg-related entries from pnpm-lock.yaml, which
  pnpm did not remove on its own because drizzle-orm still lists pg
  as an optional peer.

drizzle-orm/pg-core imports are the Drizzle dialect module, not the
pg package, and remain.

Verified with mise run check/build/test, a PGlite startup, and a
PostgreSQL 17 container (migrations applied, GraphQL responding).

AI assistance: Claude Code drafted the driver replacement, the
credential type changes, the lockfile pruning, and this message; the
result was reviewed and verified by a human.

Assisted-by: Claude Code:claude-fable-5-1
createYogaServer() used to register the ActivityPub dispatchers on the
Federation instance it received, so calling it twice with the same
instance crashed with a RouterError from Fedify's duplicate-route check.
The constraint was not documented on the option either.

Move the registration into the Federation factory itself:

- packages/graphql/src/federation.ts now exports buildFederation(), which
  registers every dispatcher and listener on a fresh Fedify
  FederationBuilder, and a default createFederation() that builds the
  Federation from that builder with the given options.
- createYogaServer() takes the built Federation as its second positional
  argument and only stores it in the resolver context.  The federation
  field is removed from YogaServerOptions, which is optional again.
- @drfed/graphql exposes the ./federation subpath so that @drfed/drfed
  can build the Federation at startup.
- The test harness builds a Federation with MemoryKvStore and exposes it;
  federation.test.ts checks the URI layout, that one builder yields
  independent instances, and that createYogaServer() no longer mutates
  the instance it is given.

This commit was produced with AI assistance.  The approach (moving the
dispatcher registration to the Federation factory and using Fedify's
FederationBuilder) and the second-positional-argument signature were
chosen by the human author; the assistant wrote the code, tests, and
documentation updates, and verified them with the mise check, build,
and test tasks and a manual server start.

Assisted-by: Claude Code:claude-fable-5-1
Apply the fixes from an AI-assisted code review of the create-actor
branch (fedify-dev#47).

 -  Mount the Fedify federation in front of the Yoga server.  The srvx
    fetch handler now calls federation.fetch() and falls back to Yoga
    via onNotFound and onNotAcceptable, so the actor, WebFinger, inbox,
    and collection dispatchers are reachable over HTTP instead of only
    serving as URI builders.
 -  Close the database client (PGlite or postgres.js) during shutdown
    before exiting the process.
 -  Serialize genActors per instance by locking the local_instances row
    with SELECT ... FOR UPDATE before counting actors.  This closes the
    race where two concurrent calls both passed the maxActors check
    under READ COMMITTED.  The post-insert recount, tx.rollback(), the
    outer mutable state, and the string-matched catch block are gone;
    every error path now returns a result object directly from the
    transaction callback.
 -  Derive the instance host from local_instances.slug and ctx.root
    rather than reading instances.host.
 -  Generate one UUID per actor and reuse it for local_actors.id,
    actors.id, actors.localId, and the placeholder username, dropping
    the .returning() round trip on the local_actors insert.
 -  Push the local-actor and host predicates into the Drizzle relational
    where clause in findLocalActor and mapHandle, replacing the full-row
    load plus JS filtering and the two-step instance/actor lookup.
 -  Import hashSecret from auth/hash.ts in actor.test.ts and
    instance.test.ts instead of keeping duplicate copies.

Provenance: the contributor asked Claude Code to review the branch
against the pull request comments and the overall diff.  Claude Code
produced the review findings (unmounted federation routes, the
maxActors race, the unclosed database client, the duplicated lookups,
and the duplicated test helper) and drafted this commit message.  The
contributor implemented every change in this commit by hand and
verified them with mise run check and the package test suites.

Assisted-by: Claude Code:claude-fable-5-1
Code review objected to abbreviated identifiers.  Rename the
`Mutation.genActors` field to `Mutation.generateActors` and update
the test's operation name, query constant, and assertions to match.

The rename was applied by a subagent and reviewed by the assistant,
which also caught the remaining `GenActors` operation name in the test.

fedify-dev#47 (comment)

Assisted-by: Claude Code:claude-fable-5-1
@2chanhaeng

Copy link
Copy Markdown
Member Author

Conflict should be resolved first

Resolved!

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.

GraphQL API for creating actors

4 participants