Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7976abd
Add ActivityPub object creation and outbox GraphQL support
2chanhaeng Sep 6, 2026
70c10a5
Ignore local mise configuration
2chanhaeng Sep 6, 2026
af600b6
Cleanup AI genned
2chanhaeng Sep 8, 2026
278001d
Fix type errors reported by mise run check
2chanhaeng Sep 11, 2026
4c39888
Sort .gitignore
2chanhaeng Sep 12, 2026
1f88ebe
Add tests hiding deleted nodes from node and nodes
2chanhaeng Sep 14, 2026
45d546d
Filter deleted records
2chanhaeng Sep 14, 2026
be79726
Add Create Dispatcher
2chanhaeng Sep 14, 2026
4b5e836
Test the Create activity dispatcher
2chanhaeng Sep 14, 2026
f5c54e6
ActivityPub addressing preservation and Create activity persistence
2chanhaeng Sep 15, 2026
21a8878
Add failing tests reproducing PR #73 review findings
2chanhaeng Sep 15, 2026
412a045
Maintain timestamp precision and hide soft-deleted resources
2chanhaeng Sep 15, 2026
168955a
Squash unreleased ActivityPub resource migrations
2chanhaeng Sep 16, 2026
5de427e
Fill outbox collections and hide deleted actors' collections
2chanhaeng Sep 16, 2026
925acbe
Split collection role, count, and addressing IRI
2chanhaeng Sep 16, 2026
4dad970
Stabilize resource nodes and align activity and outbox queries
2chanhaeng Sep 17, 2026
b328929
Track FIXME comments with issue links
2chanhaeng Sep 17, 2026
57d3e90
Index activities by referenced object
2chanhaeng Sep 18, 2026
3d32b3d
Page collection items with keyset cursors
2chanhaeng Sep 18, 2026
193b0ca
Preserve configured origins for ActivityPub resources
2chanhaeng Sep 18, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ __generated__/

.pgdata/

mise.local.toml
node_modules/
packages/*/dist/

Expand Down
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"eslint/max-lines": "off",
"eslint/id-length": ["warn", { "exceptionPatterns": ["^_", "^[Tertv]$"] }],
"eslint/init-declarations": "off",
"eslint/max-params": ["warn", { "max": 4 }],
"eslint/max-params": "off",
"eslint/max-statements": ["warn", { "max": 20 }],
"eslint/no-console": "warn",
"eslint/no-use-before-define": "off",
Expand Down
1 change: 1 addition & 0 deletions AI_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Assisted-by: OpenCode:qwen3.6-plus
Assisted-by: Claude Code:claude-sonnet-5
Assisted-by: Antigravity:gemini-3.7-flash
Assisted-by: Codex:gpt-5.6-sol
Assisted-by: Codex:gpt-6-astra
~~~~

If multiple AI tools were used, include one `Assisted-by` line per tool.
Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ Use existing dependencies and patterns before adding new ones. In particular:
Formatting is handled by [Oxfmt] and [Hongdown]. Do not hand-align code in a
way that fights those tools.

When you leave a `FIXME` comment, open a GitHub issue that describes the
problem and the intended change, and put only the issue URL in the comment:

~~~~ ts
// FIXME: https://github.com/fedify-dev/drfed/issues/35
~~~~

Keep the details in the issue rather than in the comment, and update the issue
when the plan changes.

[Optique]: https://optique.dev/
[srvx]: https://srvx.h3.dev/
[Drizzle ORM]: https://orm.drizzle.team/
Expand Down
2 changes: 1 addition & 1 deletion packages/drfed/src/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const sessionId = "00000000-0000-4000-8000-000000000000";
const accountId = "00000000-0000-4000-8000-000000000001";
const memberId = "00000000-0000-4000-8000-000000000002";
const pendingMemberId = "00000000-0000-4000-8000-000000000003";
const created = new Date("2026-06-24T00:00:00.000Z");
const created = Temporal.Instant.from("2026-06-24T00:00:00.000Z");
const tokenHash = "dev-token-hash";

async function seedAccounts(db: Database): Promise<void> {
Expand Down
9 changes: 4 additions & 5 deletions packages/drfed/src/serving.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ import {
warnAboutStrandedInstances,
} from "@drfed/drfed/serving";
import { migrate, relations, schema } from "@drfed/models";
import { uuidV7 } from "@drfed/models/uuid";
import { uuidV7 as uuid } from "@drfed/models/uuid";
import { PGlite } from "@electric-sql/pglite";
import { describe, it } from "@logtape/testing-node/autoload";
import { drizzle } from "drizzle-orm/pglite";

const rootOrigin = new URL("https://drfed.net");
const dayInMilliseconds = 86_400_000;

/**
* A stand-in for the request object a server adapter hands the handler, whose
Expand Down Expand Up @@ -204,10 +203,10 @@ describe("findStrandedInstances()", () => {
// the stored host is consulted.
{ host: "999.1.1.1", slug: "unparseable", local: true },
];
const expires = new Date(Date.now() + dayInMilliseconds);
const expires = Temporal.Now.instant().add({ hours: 24 });
const seeded = rows.map(({ host, slug, local }) => ({
host,
localId: local ? uuidV7() : null,
localId: local ? uuid() : null,
slug,
}));
await db
Expand All @@ -220,7 +219,7 @@ describe("findStrandedInstances()", () => {
await db
.insert(schema.instances)
.values(
seeded.map(({ host, localId }) => ({ id: uuidV7(), localId, host })),
seeded.map(({ host, localId }) => ({ id: uuid(), localId, host })),
);

const stranded = await findStrandedInstances(db, rootOrigin);
Expand Down
10 changes: 10 additions & 0 deletions packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
"types": "./dist/builder.d.mts",
"default": "./dist/builder.mjs"
},
"./classification": {
"types": "./dist/classification.d.mts",
"default": "./dist/classification.mjs"
},
"./federation": {
"types": "./dist/federation.d.mts",
"default": "./dist/federation.mjs"
Expand All @@ -70,6 +74,10 @@
"types": "./dist/schema.d.mts",
"default": "./dist/schema.mjs"
},
"./object": {
"types": "./dist/object.d.mts",
"default": "./dist/object.mjs"
},
"./origin": {
"types": "./dist/origin.d.mts",
"default": "./dist/origin.mjs"
Expand All @@ -85,9 +93,11 @@
"src/account.ts",
"src/actor.ts",
"src/builder.ts",
"src/classification.ts",
"src/federation.ts",
"src/instance.ts",
"src/schema.ts",
"src/object.ts",
"src/origin.ts"
],
"dts": {
Expand Down
61 changes: 56 additions & 5 deletions packages/graphql/src/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { describe, it } from "@logtape/testing-node/autoload";

import { withTestHarness } from "./harness.test.ts";

const accepted = new Date("2026-06-24T00:00:00.000Z");
const created = new Date("2026-06-24T00:00:00.000Z");
const expires = new Date("2026-07-24T00:00:00.000Z");
const accepted = Temporal.Instant.from("2026-06-24T00:00:00.000Z");
const created = Temporal.Instant.from("2026-06-24T00:00:00.000Z");
const expires = Temporal.Instant.from("2026-07-24T00:00:00.000Z");
const ok = 200;

const accountId = "00000000-0000-4000-8000-000000000001";
Expand Down Expand Up @@ -116,8 +116,8 @@ const accountInstancesResponse = {
totalCount: 1,
edges: [
{
created: "2026-06-24T00:00:00.000Z",
accepted: "2026-06-24T00:00:00.000Z",
created: created.toString(),
accepted: accepted.toString(),
admin: true,
node: {
uuid: acceptedInstanceId,
Expand Down Expand Up @@ -463,3 +463,54 @@ async function seedLocalInstances(db: Database): Promise<void> {
},
]);
}

// Cursor requests depend on the preceding page.
// oxlint-disable no-await-in-loop
it("paginates both membership connections with identical microsecond timestamps", async () => {
await withTestHarness(async ({ db, post }) => {
await seedMembershipGraph(db);
await db.update(schema.instanceMembers).set({
accepted,
created: Temporal.Instant.from("2026-09-14T12:00:00.123456Z"),
});
const auth = await createSession(db);
for (const scenario of [
{
type: "Account",
id: accountId,
field: "instances",
ids: [pendingInstanceId, acceptedInstanceId],
},
{
type: "Instance",
id: acceptedInstanceId,
field: "members",
ids: [pendingMemberId, memberId, accountId],
},
]) {
const seen: string[] = [];
let after: string | null = null;
for (const expected of scenario.ids) {
const response = await post(
{
query: `query($id: ID!, $after: String) { node(id: $id) { ... on ${scenario.type} { ${scenario.field}(first: 1, after: $after) { edges { cursor node { uuid } } pageInfo { hasNextPage } } } } }`,
variables: { id: btoa(`${scenario.type}:${scenario.id}`), after },
},
auth,
);
const body = await response.json();
assert.equal(body.errors, undefined);
const connection = body.data.node[scenario.field];
assert.equal(connection.edges.length, 1);
assert.equal(connection.edges[0].node.uuid, expected);
seen.push(connection.edges[0].node.uuid);
assert.equal(
connection.pageInfo.hasNextPage,
seen.length < scenario.ids.length,
);
after = connection.edges[0].cursor;
}
assert.deepEqual(seen, scenario.ids);
}
});
});
10 changes: 2 additions & 8 deletions packages/graphql/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,13 @@ const accountInstancesConnection = drizzleConnectionHelpers(
"instanceMembers",
{
query: {
orderBy: { created: "desc" },
orderBy: { created: "desc", instanceId: "desc" },
},
select(nestedSelection) {
return {
with: {
instance: nestedSelection(),
},
where: {
accepted: { isNotNull: true },
},
};
},
resolveNode(instanceMember) {
Expand Down Expand Up @@ -211,16 +208,13 @@ const instanceMembersConnection = drizzleConnectionHelpers(
"instanceMembers",
{
query: {
orderBy: { created: "desc" },
orderBy: { created: "desc", accountId: "desc" },
},
select(nestedSelection) {
return {
with: {
account: nestedSelection(),
},
where: {
accepted: { isNotNull: true },
},
};
},
resolveNode(instanceMember) {
Expand Down
Loading
Loading