diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 9b2e908..d1b6fcd 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -119,6 +119,7 @@ }, "dependencies": { "@drfed/models": "workspace:*", + "@faker-js/faker": "catalog:", "@fedify/uri-template": "^2.3.1", "@fedify/fedify": "catalog:", "@fedify/vocab": "catalog:", diff --git a/packages/graphql/src/actor.test.ts b/packages/graphql/src/actor.test.ts index 65670cc..19a86c0 100644 --- a/packages/graphql/src/actor.test.ts +++ b/packages/graphql/src/actor.test.ts @@ -22,6 +22,7 @@ import assert from "node:assert/strict"; import { schema } from "@drfed/models"; import { type Uuid, uuidV7 as uuid } from "@drfed/models/uuid"; +import { faker } from "@faker-js/faker"; import { describe, it } from "@logtape/testing-node/autoload"; import { and, eq } from "drizzle-orm"; @@ -143,7 +144,14 @@ describe("Mutation.generateActors", () => { }); }); - it("creates local actors", async () => { + it("creates local actors with normalized usernames and retries collisions", async (t) => { + const username = t.mock.method( + faker.internet, + "username", + () => "Dr.FED42", + ); + username.mock.mockImplementationOnce(() => "Dr-Fed43", 2); + await withTestHarness(async ({ db, post }) => { const auth = await seedAuthenticatedLocalInstance(db); @@ -189,6 +197,18 @@ describe("Mutation.generateActors", () => { const actors = await db.select().from(schema.actors); assert.equal(actors.length, 2); + assert.equal(username.mock.callCount(), 3); + assert.deepEqual( + new Set(actors.map((actor) => actor.username)), + new Set(["dr_fed42", "dr_fed43"]), + ); + for (const actor of actors) { + assert.match(actor.username, /^[a-z0-9_]+$/u); + assert.equal( + actor.profileUrl, + `https://test-instance.drfed.org/@${actor.username}`, + ); + } assert.equal( actors.every( (actor) => diff --git a/packages/graphql/src/actor.ts b/packages/graphql/src/actor.ts index 6849692..f11dfdc 100644 --- a/packages/graphql/src/actor.ts +++ b/packages/graphql/src/actor.ts @@ -22,6 +22,7 @@ import { type Database, promoteResource, schema } from "@drfed/models"; import { type CollectionRole, actorTypeEnum } from "@drfed/models/schema"; import { type Uuid, uuidV7 as uuid } from "@drfed/models/uuid"; +import { faker } from "@faker-js/faker"; import type { Context } from "@fedify/fedify"; import { drizzleConnectionHelpers } from "@pothos/plugin-drizzle"; import type { PgInsertValue } from "drizzle-orm/pg-core"; @@ -358,14 +359,19 @@ builder.mutationFields((t) => ({ fedCtx.getActorUri(id).href, "actor", async (inner, resource) => { - const [createdActor] = await inner - .insert(schema.actors) - .values(generateActor(resource.id, targetInstanceId, fedCtx)) - .returning(); - if (createdActor == null) { - throw new Error("Actor insertion returned no row."); + for (let attempt = 0; attempt < 10; attempt += 1) { + const [createdActor] = await inner + .insert(schema.actors) + .values(generateActor(resource.id, targetInstanceId, fedCtx)) + .onConflictDoNothing({ + target: [schema.actors.username, schema.actors.instanceId], + }) + .returning(); + if (createdActor != null) { + return createdActor; + } } - return createdActor; + throw new Error("Actor insertion returned no row."); }, id, ); @@ -406,15 +412,19 @@ function generateActor( instanceId: Uuid, fedCtx: Context, ): PgInsertValue { + const username = faker.internet + .username() + .toLowerCase() + .replace(/[^a-z0-9_]/gu, "_"); + return { id, localId: id, - // FIXME: https://github.com/fedify-dev/drfed/issues/85 - username: id, + username, instanceId, type: "Person", inboxUrl: fedCtx.getInboxUri(id).href, - profileUrl: new URL(`/@${id}`, fedCtx.origin).href, + profileUrl: new URL(`/@${username}`, fedCtx.origin).href, }; } diff --git a/packages/web/package.json b/packages/web/package.json index 8199551..dc88a13 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -46,6 +46,7 @@ "preview": "vite preview" }, "dependencies": { + "@faker-js/faker": "catalog:", "@formisch/solid": "1.0.0", "@kobalte/core": "^0.13.13", "@solidjs/meta": "^0.29.4", @@ -60,7 +61,6 @@ "vite": "^8.2.0" }, "devDependencies": { - "@faker-js/faker": "^10.5.0", "@types/relay-runtime": "^20.1.1", "eslint-plugin-solid": "^0.14.5", "relay-compiler": "^21.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 795d301..2eea2ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ catalogs: '@electric-sql/pglite': specifier: ^0.5.3 version: 0.5.3 + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 '@fedify/fedify': specifier: 2.4.0-pr.1020.43+41cebe9c version: 2.4.0-pr.1020.43 @@ -171,6 +174,9 @@ importers: '@drfed/models': specifier: workspace:* version: link:../models + '@faker-js/faker': + specifier: 'catalog:' + version: 10.5.0 '@fedify/fedify': specifier: 'catalog:' version: 2.4.0-pr.1020.43 @@ -275,6 +281,9 @@ importers: packages/web: dependencies: + '@faker-js/faker': + specifier: 'catalog:' + version: 10.5.0 '@formisch/solid': specifier: 1.0.0 version: 1.0.0(solid-js@1.9.14)(typescript@7.0.2)(valibot@1.4.2(typescript@7.0.2)) @@ -312,9 +321,6 @@ importers: specifier: ^8.2.0 version: 8.2.0(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) devDependencies: - '@faker-js/faker': - specifier: ^10.5.0 - version: 10.5.0 '@types/relay-runtime': specifier: ^20.1.1 version: 20.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 469756d..3ce0a1d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,7 @@ allowBuilds: catalog: "@electric-sql/pglite": ^0.5.3 + "@faker-js/faker": ^10.5.0 "@fedify/fedify": 2.4.0-pr.1020.43+41cebe9c "@fedify/postgres": 2.4.0-pr.1020.43+41cebe9c "@fedify/pglite": 2.4.0-pr.1020.43+41cebe9c