Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 3 additions & 6 deletions src/services/extensions/v2/developers-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ function parseDeveloperRow(row: Record<string, unknown>): 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions src/services/extensions/v2/extensions-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -134,8 +134,6 @@ function parseExtensionRow(row: Record<string, unknown>): 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
Expand Down
4 changes: 1 addition & 3 deletions src/services/extensions/v2/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand All @@ -60,7 +59,7 @@ export const DeveloperSchema = z
export type Developer = z.infer<typeof DeveloperSchema>;

// 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({
Expand Down Expand Up @@ -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
};
Expand Down
26 changes: 8 additions & 18 deletions test/services/extensions/v2/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -694,68 +697,59 @@ 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"
});

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"
});

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"),
Expand All @@ -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();
});
Expand Down Expand Up @@ -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",
Expand All @@ -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
});
Expand Down
10 changes: 3 additions & 7 deletions test/services/extensions/v2/mock-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down Expand Up @@ -248,15 +247,14 @@ 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 [
id,
type,
name,
url,
bio,
avatar_url,
contact_email,
owner_user_id
Expand All @@ -276,7 +274,6 @@ class MockStatement implements D1PreparedStatement {
type,
name,
url,
bio,
avatar_url,
contact_email,
owner_user_id,
Expand All @@ -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;
Expand Down
Loading