Create Object API (Note/Article only for now) - #73
2chanhaeng wants to merge 19 commits into
Conversation
Actor.objects already filters out objects whose deleted timestamp is set, but Query.node and Query.nodes still resolve deleted Actor and Object rows by primary key, including their relations. Add tests that mark one row deleted and expect node to return null and nodes to return null at that position, while a live sibling row keeps resolving. The tests currently fail on purpose: they define the expected contract for a follow-up change that adds a deleted IS NULL condition to the Actor and Object drizzleNode loaders. LocalActor is not covered because the localActors table has no deleted column. The tests were generated by Claude Code at the user's direction to address the review comment below, and the user reviewed the result. #73 (comment) Assisted-by: Claude Code:claude-fable-5-1
The outbox previously emitted Create activities whose id pointed at
a URL that nothing served, so dereferencing it returned 404. The
Create object dispatcher registered at /ap/creates/{id} in the
previous commit fixes that; this commit covers it with tests and
updates the outbox expectations to the new id and to the object
being referenced by IRI instead of embedded.
- Assert the Create URI layout from Context.getObjectUri().
- Dereference Create activities for public and unlisted objects and
check their type, id, actor, object, and recipients.
- Reject followers-only, deleted, missing, malformed, remote-actor,
deleted-actor, and wrong-host requests with 404, matching the
object dispatcher.
Addresses the review comment at
#73 (comment)
The test changes were written by an AI assistant following the
reviewer's comment and the hackerspub implementation as a reference,
and were verified by the human author by running mise run test and
mise run check.
Assisted-by: Claude Code:claude-fable-5-1
dahlia
left a comment
There was a problem hiding this comment.
The schema currently collapses ActivityPub addressing into a single visibility label, losing information that DrFed needs to inspect and compare implementations. Please preserve the original addressing and compute implementation-specific classifications for the dashboard.
The inline comments also cover timestamp precision in pagination, outbox counts, and soft-deletion behavior across relation queries. These need fixes before merging. The proposed table layout is a suggestion; preserving the protocol data is the requirement.
445c991 to
eef8ff0
Compare
dahlia
left a comment
There was a problem hiding this comment.
Please restore the referenced fixes to the PR branch and address the follow-up comments on outbox consistency and the resource/collection model.
Address the follow-up review comments on PR #73 about the resource/collection model and outbox consistency: - Drop collections.role, the collection_owner_role_key index, the GraphQL Collection.role field, and the 'public' collection_role value. Roles now live only in actor_collection_references; the Public collection is identified by its fixed UUID. Document ownerActorId as the lifecycle owner of a locally managed collection, and make Actor.outbox, followers, following, and featured return null when the referenced collection's owner is soft-deleted, matching node lookups, addressing targets, and collection items. - Add Collection.declaredTotalItems for the totalItems a collection document reports, and make Collection.totalCount always count the locally stored, visible members instead of falling back to the declared value. Document the deletion-filter policy on both fields and on Collection.items. - Add AddressingTarget.iri so the stored recipient IRI stays readable after the target or its author is deleted and target becomes null. - Document the Actor.outbox filtering policy: the GraphQL outbox keeps every stored Create activity, including those whose object is deleted, while the ActivityPub outbox serves only Public activities with live objects. Add a regression test for the deleted-object case. The unreleased migration is edited in place and its snapshot verified with drizzle-kit generate reporting no schema changes. The user wrote a per-review application plan for each review thread and requested its implementation. Codex implemented and reviewed each plan, and the user read and verified the resulting code and confirmed it by running `mise run check` and `mise run test`. #73 (comment) #73 (comment) #73 (comment) #73 (comment) Assisted-by: Codex:gpt-5.6-sol Assisted-by: Claude Code:claude-fable-5-1
eef8ff0 to
7306c18
Compare
dahlia
left a comment
There was a problem hiding this comment.
Please address the remaining comments on outbox ordering, resource identity after type discovery, and objects referenced by multiple activities.
Represent registered IRIs with stable Resource nodes and nullable typed details. Expose every referencing activity through Object.activities and compute expected classifications from the selected activity. Serialize local outbox insertions and backfill existing positions to match the ActivityPub publication order. AI provenance: The user reviewed the feedback their PR received, drafted a plan for actual application, and requested Codex to implement it along with a Fable review loop via Claude Code. Codex implemented the supplied design and regression tests. Claude Code identified missing position backfill for existing outbox rows; Codex added the migration and upgrade test in response. The user read and reviewed the completed code, then confirmed that `mise run check` and `mise run test` were successfully executed. #73 (comment) #73 (comment) #73 (comment) #73 (comment) Assisted-by: Codex:gpt-6-astra Assisted-by: Claude Code:claude-fable-5
Replace every FIXME comment in the codebase with a bare link to the
issue that tracks it, and document that rule in CONTRIBUTING.md.
- packages/graphql/src/auth/mail.ts: the two email localization notes
now point at the existing web i18n issue.
- packages/graphql/src/actor.ts: readable username generation.
- packages/web/src/routes/workspace/create/[instance_id]/actors.tsx:
result-type switch lookup tables.
- packages/graphql/src/federation.ts: actor key pairs dispatcher and
inbound activity persistence.
- packages/models/src/schema.ts: actor deletion object policy.
- CONTRIBUTING.md: when leaving a FIXME comment, open an issue and put
only the issue URL in the comment.
This applies dodok8's review suggestion on the actors.tsx FIXME (linked
below) to every FIXME comment, at the user's direction. The user also
directed turning it into a general rule in CONTRIBUTING.md.
AI provenance: The user asked Claude Code to survey every FIXME comment
and draft an issue for each. The user read and edited the drafts, then
directed Claude Code to file them through the GitHub CLI, replace the
comments with the issue URLs, and add the CONTRIBUTING.md rule. Claude
Code performed those steps and verified the result with `mise run
check` (types, lint, oxfmt, hongdown, license, versions) passing.
#73 (comment)
#35
#85
#86
#87
#88
#89
Assisted-by: Claude Code:claude-fable-5-1
dahlia
left a comment
There was a problem hiding this comment.
Please fix collection pagination when items are added between page requests. I also suggest adding an index for the object-to-activities lookup.
Implement the [#9](#9) with Note and Article storage, authenticated createObject, Relay object queries, ActivityPub dispatch, and paginated synthetic Create activities in local actor outboxes. AI provenance: The human user provided Fable with the scope and ideas for the implementation and had them draft a plan. The user read the draft, corrected any problematic parts, and had Astra handle the implementation. This session verified those changes and ran Claude Code with claude-fable-5 in a read-only review loop. After that, the human user read and verified. Automated validation: mise run check; mise run test including the full build (73 GraphQL and 3 model tests); mise run dev startup and HTTP GraphQL Object introspection. Assisted-by: Claude Code:claude-fable-5-1 Assisted-by: Codex:gpt-6-astra
Keep the pre-existing mise.local.toml ignore rule separate from the ActivityPub object feature. AI provenance: The user requested a Fable review loop. Codex reviewed and committed this existing working-tree change separately after Fable noted that it was unrelated to the feature. Codex did not author the rule. No human manual verification was confirmed. Repository checks passed. Assisted-by: Codex:gpt-6
- Rename `/users/<ACTOR_ID>/objects/<OBJECT_ID>` to `/users/<ACTOR_ID>/<OBJECT_ID>` - Rename `ASObject` to `APObject`
Brand the objects.id and objects.actorId columns with the Uuid type in @drfed/models so that Drizzle queries on the objects table require Uuid values instead of plain strings. Add validateUuid to @drfed/models/uuid as a type guard and route all UUID generation and validation in @drfed/graphql through that module instead of importing uuid directly. Cast the actor identifier in the outbox dispatcher and the test fixture values to Uuid, and declare the seed actor identifiers as const so they satisfy the branded column types. Also clarify the description of the GraphQL Object.uuid field. AI provenance: The human user authored and verified every code change in this commit. AI assistance was limited to analyzing the error messages emitted by mise run check and to drafting this commit message. Assisted-by: Claude Code:claude-fable-5-1
Actor.objects already filters out objects whose deleted timestamp is set, but Query.node and Query.nodes still resolve deleted Actor and Object rows by primary key, including their relations. Add tests that mark one row deleted and expect node to return null and nodes to return null at that position, while a live sibling row keeps resolving. The tests currently fail on purpose: they define the expected contract for a follow-up change that adds a deleted IS NULL condition to the Actor and Object drizzleNode loaders. LocalActor is not covered because the localActors table has no deleted column. The tests were generated by Claude Code at the user's direction to address the review comment below, and the user reviewed the result. #73 (comment) Assisted-by: Claude Code:claude-fable-5-1
The outbox previously emitted Create activities whose id pointed at
a URL that nothing served, so dereferencing it returned 404. The
Create object dispatcher registered at /ap/creates/{id} in the
previous commit fixes that; this commit covers it with tests and
updates the outbox expectations to the new id and to the object
being referenced by IRI instead of embedded.
- Assert the Create URI layout from Context.getObjectUri().
- Dereference Create activities for public and unlisted objects and
check their type, id, actor, object, and recipients.
- Reject followers-only, deleted, missing, malformed, remote-actor,
deleted-actor, and wrong-host requests with 404, matching the
object dispatcher.
Addresses the review comment at
#73 (comment)
The test changes were written by an AI assistant following the
reviewer's comment and the hackerspub implementation as a reference,
and were verified by the human author by running mise run test and
mise run check.
Assisted-by: Claude Code:claude-fable-5-1
Replace object classification enums with a canonical resource registry, ordered addressing occurrences, actor collections, observed memberships, and independently persisted Create activities. Backfill existing data while preserving previously published Create IRIs. Expose stored data and versioned expected classifications via GraphQL, and use explicit Public addressing for ActivityPub serving and outbox page/count selection. The user provided two PR [#73](#73) implementation plans and explicitly requested persisted Create activities. Codex implemented the models, migrations, GraphQL and federation changes, and regression tests according to the plans. The user also requested an independent Claude Fable 5 review loop. Codex applied verified review fixes for resource lock contention, shared collection migrations and role references, nullable remote outboxes, and timestamp-based outbox pagination using full database precision. Claude Code reviewed the entire changes using claude-fable-5. The user read and verified the code, and directly ran `mise test` and `mise check` to confirm they passed. Assisted-by: Codex:gpt-6 Assisted-by: Claude Code:claude-fable-5
The user requested implementation of tests for three of the five review threads from #73 that demonstrate the issues actually exist and do not require a schema redesign: - Cursor precision (#73 (comment)): Actor.objects and Instance.actors paginated with first: 1 over rows whose published or created time carries microseconds return only the first row, because the Date mapping truncates the cursor to milliseconds. - Outbox totalItems (#73 (comment)): a followers-only object and a deleted object are both counted by the outbox collection while the outbox page returns nothing. - Soft-deleted actors (#73 (comment)): objects of a deleted actor still resolve through node, nodes, and Object.actor, and the actor is still listed by Instance.actors with its objects. The tests were generated by an AI assistant from the review threads, reviewed by the human author, and run directly with mise run test: all six new tests fail for the reasons described in the reviews and every existing test still passes. mise run check passes. #73 (comment) #73 (comment) #73 (comment) Assisted-by: Claude Code:claude-fable-5-1
Use Temporal.Instant for all timestamptz columns and GraphQL DateTime scalars. Preserve microsecond precision in composite cursors, and provide unique tie-breakers for membership connections without overwriting cursor predicates. Filter out deleted actors and their objects across nodes, relationships, addressing targets, and observed collection items/counts. Retain addressing occurrences where the target is nullable when the referenced entity is deleted. Defer deletion propagation and record future transaction and retention policy rules. During the review of PR [[#73](https://github.com/fedify-dev/drfed/issues/73)](https://github.com/fedify-dev/drfed/issues/73), the user wrote a 3-step review application plan that requires no schema changes. They requested Codex to implement this and then run an independent Claude Fable 5 review. Codex implemented the changes, aligned the plan with the existing resource/activity model, and added regression tests. Claude Code reviewed the changes using claude-fable-5, and Codex confirmed and fixed Claude Code's finding that direct LocalActor nodes exposed details of deleted actors. Codex verified `mise run check`, `mise run test` (including build, 136 tests passed), no schema differences via `mise run generate:migrate`, and HTTP 200 GraphQL and frontend responses from `mise run dev --no-seed` using a temporary local `.env` file that was subsequently removed. The human user also read and reviewed the code, and verified its operation with `mise run check` and `mise run test`. Assisted-by: Codex:gpt-6 Assisted-by: Claude Code:claude-fable-5
Replace the three migrations added on this branch since 165935e8 (add_objects, add_resources_addressing_and_activities, and add_actor_collection_references) with a single migration generated from the current schema on top of simplify_login_challenges. The resulting snapshot is structurally identical to the previous final one. The generated SQL is extended by hand in two places: the fixed identifier row for the public addressing collection that ensureResource() relies on, and registration of existing actors as resources before their iri column is dropped so databases created from main can still be upgraded. The legacy backfill of objects, collections, addressing, and Create activities is dropped along with the two tests that exercised it, since nothing has been deployed yet and there is no data to preserve. The user reviewed the changes and verified them locally by running `mise run check` and `mise run test`. #73 Assisted-by: Claude Code:claude-fable-5-1
Address the remaining review findings on the ActivityPub resource work in PR #73: - Actor.outbox.totalCount always returned 0 because createObject never recorded the Create activity as an outbox member. Add addActorCollectionItem() to @drfed/models and call it from createObject and the test seed helper, so the GraphQL collection reflects the same activities the ActivityPub outbox serves. - Drop actors.postsCount. Nothing read it and it drifted because it kept counting deleted objects; Actor.objects.totalCount already derives the count from objects.deleted. - Hide collections owned by a deleted actor from node(), nodes(), addressing targets, and collection items, matching the policy that already hides the actor itself. The ownerActor relation no longer filters deleted rows; the GraphQL owner field does so instead. - Drop addressing.target and the AddressingTarget.raw field. Nothing populated the column until incoming activities are persisted, so the field always returned null. The unreleased squashed migration is edited in place and its snapshot verified with drizzle-kit generate reporting no schema changes. The user pointed out remaining issues from previous changes and requested fixes. After reviewing the changes and identifying missing tests, they requested their addition and verified the results locally by running `mise run check`, `mise run test` once all tests were added. #73 Assisted-by: Claude Code:claude-fable-5-1
Address the follow-up review comments on PR #73 about the resource/collection model and outbox consistency: - Drop collections.role, the collection_owner_role_key index, the GraphQL Collection.role field, and the 'public' collection_role value. Roles now live only in actor_collection_references; the Public collection is identified by its fixed UUID. Document ownerActorId as the lifecycle owner of a locally managed collection, and make Actor.outbox, followers, following, and featured return null when the referenced collection's owner is soft-deleted, matching node lookups, addressing targets, and collection items. - Add Collection.declaredTotalItems for the totalItems a collection document reports, and make Collection.totalCount always count the locally stored, visible members instead of falling back to the declared value. Document the deletion-filter policy on both fields and on Collection.items. - Add AddressingTarget.iri so the stored recipient IRI stays readable after the target or its author is deleted and target becomes null. - Document the Actor.outbox filtering policy: the GraphQL outbox keeps every stored Create activity, including those whose object is deleted, while the ActivityPub outbox serves only Public activities with live objects. Add a regression test for the deleted-object case. The unreleased migration is edited in place and its snapshot verified with drizzle-kit generate reporting no schema changes. The user wrote a per-review application plan for each review thread and requested its implementation. Codex implemented and reviewed each plan, and the user read and verified the resulting code and confirmed it by running `mise run check` and `mise run test`. #73 (comment) #73 (comment) #73 (comment) #73 (comment) Assisted-by: Codex:gpt-5.6-sol Assisted-by: Claude Code:claude-fable-5-1
Represent registered IRIs with stable Resource nodes and nullable typed details. Expose every referencing activity through Object.activities and compute expected classifications from the selected activity. Serialize local outbox insertions and backfill existing positions to match the ActivityPub publication order. AI provenance: The user reviewed the feedback their PR received, drafted a plan for actual application, and requested Codex to implement it along with a Fable review loop via Claude Code. Codex implemented the supplied design and regression tests. Claude Code identified missing position backfill for existing outbox rows; Codex added the migration and upgrade test in response. The user read and reviewed the completed code, then confirmed that `mise run check` and `mise run test` were successfully executed. #73 (comment) #73 (comment) #73 (comment) #73 (comment) Assisted-by: Codex:gpt-6-astra Assisted-by: Claude Code:claude-fable-5
Replace every FIXME comment in the codebase with a bare link to the
issue that tracks it, and document that rule in CONTRIBUTING.md.
- packages/graphql/src/auth/mail.ts: the two email localization notes
now point at the existing web i18n issue.
- packages/graphql/src/actor.ts: readable username generation.
- packages/web/src/routes/workspace/create/[instance_id]/actors.tsx:
result-type switch lookup tables.
- packages/graphql/src/federation.ts: actor key pairs dispatcher and
inbound activity persistence.
- packages/models/src/schema.ts: actor deletion object policy.
- CONTRIBUTING.md: when leaving a FIXME comment, open an issue and put
only the issue URL in the comment.
This applies dodok8's review suggestion on the actors.tsx FIXME (linked
below) to every FIXME comment, at the user's direction. The user also
directed turning it into a general rule in CONTRIBUTING.md.
AI provenance: The user asked Claude Code to survey every FIXME comment
and draft an issue for each. The user read and edited the drafts, then
directed Claude Code to file them through the GitHub CLI, replace the
comments with the issue URLs, and add the CONTRIBUTING.md rule. Claude
Code performed those steps and verified the result with `mise run
check` (types, lint, oxfmt, hongdown, license, versions) passing.
#73 (comment)
#35
#85
#86
#87
#88
#89
Assisted-by: Claude Code:claude-fable-5-1
Object.activities filters activities by the referenced object and orders them by published ASC, id ASC, but the only secondary index on activities started with actorId. Add activity_object_published_index on (objectId, published, id) together with its migration. The same index also serves the ON DELETE CASCADE lookup on activities.objectId, which PostgreSQL does not index on its own. A regression test checks the index definition in a freshly migrated database. The user planned how to apply the reviews and used Claude Code to write regression tests. Afterward, they blocked the agent's access to the tests and had the plan implemented using the Codex implementation and Claude review loop. Finally, the user reviewed the changes and confirmed that `mise run check` and `mise run test` passed. #73 (comment) Assisted-by: Codex:gpt-5.6-sol Assisted-by: Claude Code:claude-fable-5-1
Collection.items used an offset cursor. Local outbox entries are prepended with min(position) - 1, so creating a post between two page requests shifted the offsets and the next page repeated an item. Use resolveCursorConnection with an opaque base64 cursor over the stored (position, itemId) ordering and query past that boundary. position is nullable, so the predicates treat NULL positions as a separate range that sorts last instead of dropping those rows, and last/before returns edges in the same order as first/after. Malformed cursors are reported to clients as validation errors. Regression tests cover a post created between page requests, NULL positions with an item prepended in the middle of a walk, and backward paging. The user planned how to apply the reviews and used Claude Code to write regression tests. Afterward, they blocked the agent's access to the tests and had the plan implemented using the Codex implementation and Claude review loop. Finally, the user reviewed the changes and confirmed that `mise run check` and `mise run test` passed. #73 (comment) Assisted-by: Codex:gpt-5.6-sol Assisted-by: Claude Code:claude-fable-5-1
55c5d99 to
3d32b3d
Compare
dahlia
left a comment
There was a problem hiding this comment.
Sorry for the additional review rounds. Please preserve the configured protocol when generating IRIs and make resource lookups independent of how the request origin is written.
| const published = Temporal.Now.instant(); | ||
| const id = uuid(); | ||
| const fedCtx = ctx.federation.createContext( | ||
| new URL(`https://${actor.host}`), |
There was a problem hiding this comment.
Could we preserve ctx.rootOrigin.protocol here, as actor generation already does? With the root origin set to http://drfed.org, I reproduced createObject returning HTTPS IRIs for both the object and its Create activity. On an HTTP-only development deployment, those IRIs point to a TLS endpoint that is not served.
Please compose this context's origin from the configured protocol and the actor's stored authority, preserving any port. A regression test should create an object under an HTTP root origin, verify the object and activity IRIs, and fetch both through their advertised URLs.
| if (!validateUuid(id)) return null; | ||
| const activity = await db.query.activities.findFirst({ | ||
| where: { | ||
| resource: { iri: ctx.getObjectUri(Create, { id }).href }, |
There was a problem hiding this comment.
These resource lookups depend on how the incoming origin is written. I reproduced both the object and Create dispatchers returning 404 Not Found when the hostname has a trailing dot, although their canonical HTTPS URLs return 200 OK. For the same host without the dot, changing the request scheme to HTTP leaves the object reachable but makes Create return 404 Not Found because this reconstructed IRI no longer matches the stored HTTPS IRI. That also affects a TLS-terminating proxy forwarding HTTP to DrFed.
Could both dispatchers apply canonicalizeAuthority(ctx.host), as findLocalActor() already does? For Create, please look up the activity by its ID within that local instance rather than by an IRI reconstructed from the request. Keep the instance restriction so another host cannot resolve it, and retain the stored canonical IRI in the response. Please cover trailing-dot hosts and an HTTP request for a stored HTTPS activity in regression tests.
Resolve #9.
Adds an objects table (Note/Article; visibility public/unlisted/followers) with its migration.
GraphQL gains a createObject mutation for authenticated instance members, a Relay Object node, and an Actor.objects connection with totalCount.
Fedify side implements an object dispatcher at /users/{identifier}/{id} (Tombstone when deleted; followers-only objects not served) and a paginated outbox of synthetic Create activities (20 per page).
Side change: ignore mise.local.toml.
Verified with mise run check, mise run test (73 GraphQL, 3 models), and mise run dev startup.
AI Disclosure: Ideas presented by a human user were refined into a plan through Claude Code (claude-fable-5-1), and the draft plan was verified and partially modified by the user. It was implemented via Codex (gpt-6-astra), and after initial verification by Claude Code (claude-fable-5-1), it was manually reviewed, verified, and partially modified by a human.