Skip to content

Commit 200cd33

Browse files
authored
Merge pull request #73 from fedify-dev/feat/obj-api
Create `Object` API (`Note`/`Article` only for now)
2 parents b9d0f4b + 193b0ca commit 200cd33

47 files changed

Lines changed: 12467 additions & 321 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ __generated__/
66

77
.pgdata/
88

9+
mise.local.toml
910
node_modules/
1011
packages/*/dist/
1112

‎.oxlintrc.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"eslint/max-lines": "off",
3434
"eslint/id-length": ["warn", { "exceptionPatterns": ["^_", "^[Tertv]$"] }],
3535
"eslint/init-declarations": "off",
36-
"eslint/max-params": ["warn", { "max": 4 }],
36+
"eslint/max-params": "off",
3737
"eslint/max-statements": ["warn", { "max": 20 }],
3838
"eslint/no-console": "warn",
3939
"eslint/no-use-before-define": "off",

‎AI_POLICY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Assisted-by: OpenCode:qwen3.6-plus
102102
Assisted-by: Claude Code:claude-sonnet-5
103103
Assisted-by: Antigravity:gemini-3.7-flash
104104
Assisted-by: Codex:gpt-5.6-sol
105+
Assisted-by: Codex:gpt-6-astra
105106
~~~~
106107

107108
If multiple AI tools were used, include one `Assisted-by` line per tool.

‎CONTRIBUTING.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@ Use existing dependencies and patterns before adding new ones. In particular:
237237
Formatting is handled by [Oxfmt] and [Hongdown]. Do not hand-align code in a
238238
way that fights those tools.
239239

240+
When you leave a `FIXME` comment, open a GitHub issue that describes the
241+
problem and the intended change, and put only the issue URL in the comment:
242+
243+
~~~~ ts
244+
// FIXME: https://github.com/fedify-dev/drfed/issues/35
245+
~~~~
246+
247+
Keep the details in the issue rather than in the comment, and update the issue
248+
when the plan changes.
249+
240250
[Optique]: https://optique.dev/
241251
[srvx]: https://srvx.h3.dev/
242252
[Drizzle ORM]: https://orm.drizzle.team/

‎packages/drfed/src/seed.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const sessionId = "00000000-0000-4000-8000-000000000000";
2626
const accountId = "00000000-0000-4000-8000-000000000001";
2727
const memberId = "00000000-0000-4000-8000-000000000002";
2828
const pendingMemberId = "00000000-0000-4000-8000-000000000003";
29-
const created = new Date("2026-06-24T00:00:00.000Z");
29+
const created = Temporal.Instant.from("2026-06-24T00:00:00.000Z");
3030
const tokenHash = "dev-token-hash";
3131

3232
async function seedAccounts(db: Database): Promise<void> {

‎packages/drfed/src/serving.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@ import {
2222
warnAboutStrandedInstances,
2323
} from "@drfed/drfed/serving";
2424
import { migrate, relations, schema } from "@drfed/models";
25-
import { uuidV7 } from "@drfed/models/uuid";
25+
import { uuidV7 as uuid } from "@drfed/models/uuid";
2626
import { PGlite } from "@electric-sql/pglite";
2727
import { describe, it } from "@logtape/testing-node/autoload";
2828
import { drizzle } from "drizzle-orm/pglite";
2929

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

3332
/**
3433
* A stand-in for the request object a server adapter hands the handler, whose
@@ -204,10 +203,10 @@ describe("findStrandedInstances()", () => {
204203
// the stored host is consulted.
205204
{ host: "999.1.1.1", slug: "unparseable", local: true },
206205
];
207-
const expires = new Date(Date.now() + dayInMilliseconds);
206+
const expires = Temporal.Now.instant().add({ hours: 24 });
208207
const seeded = rows.map(({ host, slug, local }) => ({
209208
host,
210-
localId: local ? uuidV7() : null,
209+
localId: local ? uuid() : null,
211210
slug,
212211
}));
213212
await db
@@ -220,7 +219,7 @@ describe("findStrandedInstances()", () => {
220219
await db
221220
.insert(schema.instances)
222221
.values(
223-
seeded.map(({ host, localId }) => ({ id: uuidV7(), localId, host })),
222+
seeded.map(({ host, localId }) => ({ id: uuid(), localId, host })),
224223
);
225224

226225
const stranded = await findStrandedInstances(db, rootOrigin);

‎packages/graphql/package.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858
"types": "./dist/builder.d.mts",
5959
"default": "./dist/builder.mjs"
6060
},
61+
"./classification": {
62+
"types": "./dist/classification.d.mts",
63+
"default": "./dist/classification.mjs"
64+
},
6165
"./federation": {
6266
"types": "./dist/federation.d.mts",
6367
"default": "./dist/federation.mjs"
@@ -70,6 +74,10 @@
7074
"types": "./dist/schema.d.mts",
7175
"default": "./dist/schema.mjs"
7276
},
77+
"./object": {
78+
"types": "./dist/object.d.mts",
79+
"default": "./dist/object.mjs"
80+
},
7381
"./origin": {
7482
"types": "./dist/origin.d.mts",
7583
"default": "./dist/origin.mjs"
@@ -85,9 +93,11 @@
8593
"src/account.ts",
8694
"src/actor.ts",
8795
"src/builder.ts",
96+
"src/classification.ts",
8897
"src/federation.ts",
8998
"src/instance.ts",
9099
"src/schema.ts",
100+
"src/object.ts",
91101
"src/origin.ts"
92102
],
93103
"dts": {

‎packages/graphql/src/account.test.ts‎

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import { describe, it } from "@logtape/testing-node/autoload";
2222

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

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

3030
const accountId = "00000000-0000-4000-8000-000000000001";
@@ -116,8 +116,8 @@ const accountInstancesResponse = {
116116
totalCount: 1,
117117
edges: [
118118
{
119-
created: "2026-06-24T00:00:00.000Z",
120-
accepted: "2026-06-24T00:00:00.000Z",
119+
created: created.toString(),
120+
accepted: accepted.toString(),
121121
admin: true,
122122
node: {
123123
uuid: acceptedInstanceId,
@@ -463,3 +463,54 @@ async function seedLocalInstances(db: Database): Promise<void> {
463463
},
464464
]);
465465
}
466+
467+
// Cursor requests depend on the preceding page.
468+
// oxlint-disable no-await-in-loop
469+
it("paginates both membership connections with identical microsecond timestamps", async () => {
470+
await withTestHarness(async ({ db, post }) => {
471+
await seedMembershipGraph(db);
472+
await db.update(schema.instanceMembers).set({
473+
accepted,
474+
created: Temporal.Instant.from("2026-09-14T12:00:00.123456Z"),
475+
});
476+
const auth = await createSession(db);
477+
for (const scenario of [
478+
{
479+
type: "Account",
480+
id: accountId,
481+
field: "instances",
482+
ids: [pendingInstanceId, acceptedInstanceId],
483+
},
484+
{
485+
type: "Instance",
486+
id: acceptedInstanceId,
487+
field: "members",
488+
ids: [pendingMemberId, memberId, accountId],
489+
},
490+
]) {
491+
const seen: string[] = [];
492+
let after: string | null = null;
493+
for (const expected of scenario.ids) {
494+
const response = await post(
495+
{
496+
query: `query($id: ID!, $after: String) { node(id: $id) { ... on ${scenario.type} { ${scenario.field}(first: 1, after: $after) { edges { cursor node { uuid } } pageInfo { hasNextPage } } } } }`,
497+
variables: { id: btoa(`${scenario.type}:${scenario.id}`), after },
498+
},
499+
auth,
500+
);
501+
const body = await response.json();
502+
assert.equal(body.errors, undefined);
503+
const connection = body.data.node[scenario.field];
504+
assert.equal(connection.edges.length, 1);
505+
assert.equal(connection.edges[0].node.uuid, expected);
506+
seen.push(connection.edges[0].node.uuid);
507+
assert.equal(
508+
connection.pageInfo.hasNextPage,
509+
seen.length < scenario.ids.length,
510+
);
511+
after = connection.edges[0].cursor;
512+
}
513+
assert.deepEqual(seen, scenario.ids);
514+
}
515+
});
516+
});

‎packages/graphql/src/account.ts‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,13 @@ const accountInstancesConnection = drizzleConnectionHelpers(
9090
"instanceMembers",
9191
{
9292
query: {
93-
orderBy: { created: "desc" },
93+
orderBy: { created: "desc", instanceId: "desc" },
9494
},
9595
select(nestedSelection) {
9696
return {
9797
with: {
9898
instance: nestedSelection(),
9999
},
100-
where: {
101-
accepted: { isNotNull: true },
102-
},
103100
};
104101
},
105102
resolveNode(instanceMember) {
@@ -211,16 +208,13 @@ const instanceMembersConnection = drizzleConnectionHelpers(
211208
"instanceMembers",
212209
{
213210
query: {
214-
orderBy: { created: "desc" },
211+
orderBy: { created: "desc", accountId: "desc" },
215212
},
216213
select(nestedSelection) {
217214
return {
218215
with: {
219216
account: nestedSelection(),
220217
},
221-
where: {
222-
accepted: { isNotNull: true },
223-
},
224218
};
225219
},
226220
resolveNode(instanceMember) {

0 commit comments

Comments
 (0)