Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
38 changes: 38 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,41 @@ introduced.
The package boundary tests in *test/package-boundaries.test.ts* compare core
package manifests with their production module graphs and reject workspace
dependency cycles.


Post feature boundaries
-----------------------

The models package exposes post behavior through focused public subpaths. New
code should import the narrowest applicable module instead of the compatibility
facade at *@hackerspub/models/post*:

| Subpath | Responsibility |
| ------------------------------------ | --------------------------------------------------- |
| *@hackerspub/models/post/core* | Post object guards and persisted post lookup |
| *@hackerspub/models/post/remote* | Remote ActivityPub ingestion and deletion |
| *@hackerspub/models/post/source* | Local article, note, and question synchronization |
| *@hackerspub/models/post/visibility* | Visibility, moderation, and interaction policies |
| *@hackerspub/models/post/sharing* | Local share and unshare operations |
| *@hackerspub/models/post/engagement* | Reply, share, and quote counters and quote revoking |
| *@hackerspub/models/post/lifecycle* | Local post deletion |
| *@hackerspub/models/link-preview* | Link scraping, persistence, and repair |

*models/post.ts* remains an explicit re-export facade for existing consumers.
It contains no post behavior. Article source rendering helpers live in
*models/article-source.ts* so local post synchronization does not form a cycle
with the article model.

The GraphQL post schema follows the same registration boundaries under
*graphql/post/*. *core.ts* owns the `Post` interface, shared fields, media, and
link types; *article.ts* owns article and draft types; *note.ts* owns note and
question types; and *mutations.ts* owns post mutations and queries. Actor post
fields and `Account.articleDrafts` are registered by *actor-fields.ts* and
*article-fields.ts* from the GraphQL composition root after their base types are
available. This avoids `actor` or `account` importing the post facade.

*graphql/post.ts* is a compatibility facade that preserves the historical
public type references. Adding a post feature means registering it in a
focused module and importing that registration from *graphql/mod.ts*; schema
generation must remain deterministic and must not change merely because a
registration moved between modules.
2 changes: 1 addition & 1 deletion federation/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { removeHeaderAnchorLinks } from "@hackerspub/models/html";
import {
getSanctionHiddenActorFilter,
isActorSanctionHidden,
} from "@hackerspub/models/post";
} from "@hackerspub/models/post/visibility";
import {
actorTable,
followingTable,
Expand Down
9 changes: 4 additions & 5 deletions federation/inbox/quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ import {
} from "@hackerspub/models/actor";
import type { ContextData } from "@hackerspub/models/context";
import { toApplicationContext } from "../context.ts";
import { isPostObject, type PostObject } from "@hackerspub/models/post/core";
import { updateQuotesCount } from "@hackerspub/models/post/engagement";
import { persistPost } from "@hackerspub/models/post/remote";
import {
canActorQuotePost,
canActorRequestQuotePost,
getOriginalPostId,
isPostObject,
persistPost,
type PostObject,
updateQuotesCount,
} from "@hackerspub/models/post";
} from "@hackerspub/models/post/visibility";
import {
type Actor,
type Blocking,
Expand Down
6 changes: 3 additions & 3 deletions federation/inbox/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ import {
import {
deletePersistedPost,
deleteSharedPost,
isPostObject,
PERSIST_POST_OVERALL_BUDGET_MS,
persistPost,
persistSharedPost,
REMOTE_FETCH_TIMEOUT_MS,
updateRepliesCount,
withDocumentLoaderTimeout,
} from "@hackerspub/models/post";
} from "@hackerspub/models/post/remote";
import { isPostObject } from "@hackerspub/models/post/core";
import { updateRepliesCount } from "@hackerspub/models/post/engagement";
Comment thread
dahlia marked this conversation as resolved.
Outdated
import { persistPollVoteResult } from "@hackerspub/models/poll";
import {
deleteReaction,
Expand Down
2 changes: 1 addition & 1 deletion federation/inbox/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { InboxContext } from "@fedify/fedify";
import { isActor, type Update } from "@fedify/vocab";
import { isCachedActorFederationBlocked } from "@hackerspub/models/actor";
import type { ContextData } from "@hackerspub/models/context";
import { isPostObject } from "@hackerspub/models/post";
import { isPostObject } from "@hackerspub/models/post/core";
import { getLogger } from "@logtape/logtape";
import { onActorUpdated } from "./actor.ts";
import { onPostUpdated } from "./subscribe.ts";
Expand Down
2 changes: 1 addition & 1 deletion federation/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
isActorSanctionHidden,
isPostVisibleTo,
normalizeQuotePolicyForVisibility,
} from "@hackerspub/models/post";
} from "@hackerspub/models/post/visibility";
import type {
Account,
Actor,
Expand Down
19 changes: 0 additions & 19 deletions graphql/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import { InvitationLink } from "./invitation-link.ts";
import { lookupActorByUrl, parseHttpUrl } from "./lookup.ts";
import { Notification } from "./notification.ts";
import { putProfileOgImage } from "./og.ts";
import { ArticleDraft } from "./post.ts";
import {
fromPostVisibility,
PostVisibility,
Expand Down Expand Up @@ -895,24 +894,6 @@ builder.drizzleObjectField(Account, "invitees", (t) =>
},
));

builder.drizzleObjectField(
Account,
"articleDrafts",
(t) =>
t.relatedConnection("articleDrafts", {
type: ArticleDraft,
description:
"Unpublished article drafts belonging to this account, most " +
"recently updated first. Only visible to the account holder.",
authScopes: (parent) => ({
selfAccount: "id" in parent ? parent.id : undefined,
}),
query: () => ({
orderBy: { updated: "desc" },
}),
}),
);

// Per-request batching loader for Account.postCount and
// Account.lastPostPublished. Without this, requesting these fields on a
// 50-row connection would fan out to 100 separate aggregate queries.
Expand Down
Loading
Loading