diff --git a/src/services/extensions/v2/db/migrations/0010_drop_developer_bio.sql b/src/services/extensions/v2/db/migrations/0010_drop_developer_bio.sql new file mode 100644 index 0000000..4b3939e --- /dev/null +++ b/src/services/extensions/v2/db/migrations/0010_drop_developer_bio.sql @@ -0,0 +1,4 @@ +-- v2: drop developers.bio — this profile field turned out not to be useful +-- and is being removed from the API surface entirely. + +ALTER TABLE developers DROP COLUMN bio; diff --git a/src/services/extensions/v2/developers-database.ts b/src/services/extensions/v2/developers-database.ts index d858f8f..afc42b8 100644 --- a/src/services/extensions/v2/developers-database.ts +++ b/src/services/extensions/v2/developers-database.ts @@ -52,7 +52,6 @@ function parseDeveloperRow(row: Record): DeveloperProfile { type: row.type as DeveloperProfile["type"], name: row.name as string, URL: (row.url as string | null) ?? undefined, - bio: (row.bio as string | null) ?? undefined, avatar_url: (row.avatar_url as string | null) ?? undefined, contact_email: (row.contact_email as string | null) ?? undefined, approved: row.approved_at !== null && row.approved_at !== undefined @@ -106,15 +105,14 @@ export class DevelopersDatabase { mainStmt = this.db .prepare( - `INSERT INTO developers (id, type, name, url, bio, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)` + `INSERT INTO developers (id, type, name, url, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)` ) .bind( developer.id, developer.type, developer.name, developer.URL ?? null, - developer.bio ?? null, developer.avatar_url ?? null, developer.contact_email ?? null, userId @@ -135,14 +133,13 @@ export class DevelopersDatabase { // approval no longer applies. Not worth diffing old vs. new values. mainStmt = this.db .prepare( - `UPDATE developers SET type = ?, name = ?, url = ?, bio = ?, avatar_url = ?, contact_email = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP + `UPDATE developers SET type = ?, name = ?, url = ?, avatar_url = ?, contact_email = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?` ) .bind( developer.type, developer.name, developer.URL ?? null, - developer.bio ?? null, developer.avatar_url ?? null, developer.contact_email ?? null, developer.id diff --git a/src/services/extensions/v2/extensions-database.ts b/src/services/extensions/v2/extensions-database.ts index 57ef0f8..fa521bf 100644 --- a/src/services/extensions/v2/extensions-database.ts +++ b/src/services/extensions/v2/extensions-database.ts @@ -19,7 +19,7 @@ const SELECT_EXTENSIONS = ` e.icon_url, e.readme, e.source, e.version, e.download_url, COALESCE(d.id, e.author_id) AS developer_id, d.type AS developer_type, d.name AS developer_name, - d.url AS developer_url, d.bio AS developer_bio, + d.url AS developer_url, d.avatar_url AS developer_avatar_url, d.approved_at AS developer_approved_at FROM extensions e LEFT JOIN developers d ON e.author_id = d.id @@ -134,8 +134,6 @@ function parseExtensionRow(row: Record): Extension { name: (row.developer_name as string) ?? "", URL: typeof row.developer_url === "string" ? row.developer_url : undefined, - bio: - typeof row.developer_bio === "string" ? row.developer_bio : undefined, avatar_url: typeof row.developer_avatar_url === "string" ? row.developer_avatar_url diff --git a/src/services/extensions/v2/interfaces.ts b/src/services/extensions/v2/interfaces.ts index 4efa4f6..2210f23 100644 --- a/src/services/extensions/v2/interfaces.ts +++ b/src/services/extensions/v2/interfaces.ts @@ -51,7 +51,6 @@ export const DeveloperSchema = z type: z.enum(["user", "organization"]), name: z.string().min(1), URL: httpUrl().optional(), - bio: z.string().max(500).optional(), avatar_url: httpUrl().optional(), contact_email: z.string().email().optional() }) @@ -60,7 +59,7 @@ export const DeveloperSchema = z export type Developer = z.infer; // Submissions go through moderation and only ever touch identity fields — -// profile fields (bio/avatar_url/contact_email) are direct-write-only via +// profile fields (avatar_url/contact_email) are direct-write-only via // PUT /developers/me, so this schema rejects them instead of silently // accepting-then-dropping them when a submission is approved. export const SubmissionDeveloperSchema = DeveloperSchema.pick({ @@ -149,7 +148,6 @@ export function toPublicDeveloper(profile: DeveloperProfile): PublicDeveloper { type: profile.type, name: profile.name, URL: profile.URL, - bio: profile.bio, avatar_url: profile.avatar_url, approved: profile.approved }; diff --git a/test/services/extensions/v2/index.test.ts b/test/services/extensions/v2/index.test.ts index dc18887..005c800 100644 --- a/test/services/extensions/v2/index.test.ts +++ b/test/services/extensions/v2/index.test.ts @@ -197,13 +197,16 @@ describe("Extensions API v2", () => { expect(data.error.code).toBe("VALIDATION_ERROR"); }); - it("rejects profile fields (bio/avatar_url/contact_email) on a submission's developer", async () => { + it("rejects profile fields (avatar_url/contact_email) on a submission's developer", async () => { seedDeveloper("new-developer", "user-1"); const headers = await authHeaders("user-1"); const payload = samplePayload(); const res = await post("/extensions/v2/submissions", headers, { ...payload, - developer: { ...payload.developer, bio: "Should not be accepted here" } + developer: { + ...payload.developer, + avatar_url: "https://example.com/should-not-be-accepted.png" + } }); expect(res.status).toBe(422); @@ -694,11 +697,10 @@ describe("Extensions API v2", () => { expect(data.result.approved).toBe(false); }); - it("round-trips bio, avatar_url, and contact_email", async () => { + it("round-trips avatar_url and contact_email", async () => { const headers = await authHeaders("user-1"); const res = await put("/extensions/v2/developers/me", headers, { ...sampleDeveloper(), - bio: "I build FOSSBilling extensions.", avatar_url: "https://example.com/avatar.png", contact_email: "dev@example.com" }); @@ -706,33 +708,28 @@ describe("Extensions API v2", () => { expect(res.status).toBe(200); const data = (await res.json()) as { result: { - bio: string; avatar_url: string; contact_email: string; }; }; - expect(data.result.bio).toBe("I build FOSSBilling extensions."); expect(data.result.avatar_url).toBe("https://example.com/avatar.png"); expect(data.result.contact_email).toBe("dev@example.com"); const stored = tables.developers.get("dev-developer"); - expect(stored?.bio).toBe("I build FOSSBilling extensions."); expect(stored?.avatar_url).toBe("https://example.com/avatar.png"); expect(stored?.contact_email).toBe("dev@example.com"); }); - it("updates bio, avatar_url, and contact_email on an existing profile", async () => { + it("updates avatar_url and contact_email on an existing profile", async () => { const headers = await authHeaders("user-1"); await put("/extensions/v2/developers/me", headers, { ...sampleDeveloper(), - bio: "Old bio.", avatar_url: "https://example.com/old.png", contact_email: "old@example.com" }); const res = await put("/extensions/v2/developers/me", headers, { ...sampleDeveloper(), - bio: "New bio.", avatar_url: "https://example.com/new.png", contact_email: "new@example.com" }); @@ -740,22 +737,19 @@ describe("Extensions API v2", () => { expect(res.status).toBe(200); const data = (await res.json()) as { result: { - bio: string; avatar_url: string; contact_email: string; }; }; - expect(data.result.bio).toBe("New bio."); expect(data.result.avatar_url).toBe("https://example.com/new.png"); expect(data.result.contact_email).toBe("new@example.com"); const stored = tables.developers.get("dev-developer"); - expect(stored?.bio).toBe("New bio."); expect(stored?.avatar_url).toBe("https://example.com/new.png"); expect(stored?.contact_email).toBe("new@example.com"); }); - it("accepts a payload without bio, avatar_url, or contact_email", async () => { + it("accepts a payload without avatar_url or contact_email", async () => { const res = await put( "/extensions/v2/developers/me", await authHeaders("user-1"), @@ -765,12 +759,10 @@ describe("Extensions API v2", () => { expect(res.status).toBe(200); const data = (await res.json()) as { result: { - bio?: string; avatar_url?: string; contact_email?: string; }; }; - expect(data.result.bio).toBeUndefined(); expect(data.result.avatar_url).toBeUndefined(); expect(data.result.contact_email).toBeUndefined(); }); @@ -1599,7 +1591,6 @@ describe("Extensions API v2", () => { type: "organization", name: "Public Dev", url: "https://example.com", - bio: "We make things", avatar_url: "https://example.com/avatar.png", contact_email: "private@example.com", owner_user_id: "user-1", @@ -1616,7 +1607,6 @@ describe("Extensions API v2", () => { type: "organization", name: "Public Dev", URL: "https://example.com", - bio: "We make things", avatar_url: "https://example.com/avatar.png", approved: true }); diff --git a/test/services/extensions/v2/mock-db.ts b/test/services/extensions/v2/mock-db.ts index b6ee40d..e3ac91f 100644 --- a/test/services/extensions/v2/mock-db.ts +++ b/test/services/extensions/v2/mock-db.ts @@ -130,7 +130,6 @@ class MockStatement implements D1PreparedStatement { developer_type: developer?.type ?? "user", developer_name: developer?.name ?? "", developer_url: developer?.url ?? null, - developer_bio: developer?.bio ?? null, developer_avatar_url: developer?.avatar_url ?? null, developer_approved_at: developer?.approved_at ?? null }; @@ -248,7 +247,7 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "INSERT INTO developers (id, type, name, url, bio, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at)" + "INSERT INTO developers (id, type, name, url, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at)" ) ) { const [ @@ -256,7 +255,6 @@ class MockStatement implements D1PreparedStatement { type, name, url, - bio, avatar_url, contact_email, owner_user_id @@ -276,7 +274,6 @@ class MockStatement implements D1PreparedStatement { type, name, url, - bio, avatar_url, contact_email, owner_user_id, @@ -289,16 +286,15 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE developers SET type = ?, name = ?, url = ?, bio = ?, avatar_url = ?, contact_email = ?, approved_at = NULL" + "UPDATE developers SET type = ?, name = ?, url = ?, avatar_url = ?, contact_email = ?, approved_at = NULL" ) ) { - const [type, name, url, bio, avatar_url, contact_email, id] = p; + const [type, name, url, avatar_url, contact_email, id] = p; const row = this.tables.developers.get(String(id)); if (row) { row.type = type; row.name = name; row.url = url; - row.bio = bio; row.avatar_url = avatar_url; row.contact_email = contact_email; row.approved_at = null;