From 47f244cf546a16b878188907150dfee8f57171ee Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:31:49 +0000 Subject: [PATCH] fix(page): enforce component limit on full creation (#2660) --- .../services/src/page/__tests__/page.test.ts | 111 +++++++++++++++++- packages/services/src/page/create.ts | 6 + 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/packages/services/src/page/__tests__/page.test.ts b/packages/services/src/page/__tests__/page.test.ts index bfedf37f..e75a08d4 100644 --- a/packages/services/src/page/__tests__/page.test.ts +++ b/packages/services/src/page/__tests__/page.test.ts @@ -12,8 +12,8 @@ import { expect } from "@std/expect"; import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; import { - expectAuditRow, createWorkspaceFixture, + expectAuditRow, makeApiKeyCtx, makeUserCtx, withTestTransaction, @@ -170,6 +170,115 @@ describe("createPage (full form)", () => { }); }); + test("counts components on other pages and the full monitor batch", async () => { + await withTestTransaction(async (tx) => { + const ctx = { ...teamCtx, db: tx }; + await tx + .update(workspace) + .set({ + limits: JSON.stringify({ + ...teamCtx.workspace.limits, + "status-pages": 10, + "page-components": 2, + }), + }) + .where(eq(workspace.id, ctx.workspace.id)); + const existing = await newPage({ + ctx, + input: { title: "Existing", slug: uniqueSlug("component-existing") }, + }); + await tx.insert(pageComponent).values({ + workspaceId: ctx.workspace.id, + pageId: existing.id, + type: "static", + name: "Existing component", + }); + const secondMonitor = await tx + .insert(monitor) + .values({ + workspaceId: ctx.workspace.id, + active: true, + url: "https://example.com", + name: "Second monitor", + method: "GET", + periodicity: "10m", + regions: "ams", + }) + .returning() + .get(); + await expect( + createPage({ + ctx, + input: { + workspaceId: ctx.workspace.id, + description: "", + title: "Over limit", + slug: uniqueSlug("component-batch"), + monitors: [ + { monitorId: teamMonitorId }, + { monitorId: secondMonitor.id }, + ], + }, + }), + ).rejects.toMatchObject({ + code: "LIMIT_EXCEEDED", + max: 2, + current: 1, + }); + const allowed = await createPage({ + ctx, + input: { + workspaceId: ctx.workspace.id, + description: "", + title: "At limit", + slug: uniqueSlug("component-allowed"), + monitors: [{ monitorId: teamMonitorId }], + }, + }); + const components = await tx + .select() + .from(pageComponent) + .where(eq(pageComponent.workspaceId, ctx.workspace.id)) + .all(); + expect(components.map((c) => c.pageId).sort()).toEqual( + [existing.id, allowed.id].sort(), + ); + await expect( + createPage({ + ctx, + input: { + workspaceId: ctx.workspace.id, + description: "", + title: "Cap spent", + slug: uniqueSlug("component-spent"), + monitors: [{ monitorId: secondMonitor.id }], + }, + }), + ).rejects.toMatchObject({ + code: "LIMIT_EXCEEDED", + max: 2, + current: 2, + }); + const empty = await createPage({ + ctx, + input: { + workspaceId: ctx.workspace.id, + description: "", + title: "No components", + slug: uniqueSlug("component-empty"), + monitors: [], + }, + }); + expect( + await tx + .select() + .from(pageComponent) + .where(eq(pageComponent.pageId, empty.id)) + .all(), + ).toEqual([]); + }); + }); + test("rejects cross-workspace monitor", async () => { await withTestTransaction(async (tx) => { const slug = uniqueSlug("cross-ws"); diff --git a/packages/services/src/page/create.ts b/packages/services/src/page/create.ts index 7e4fa9ab..9efbb01e 100644 --- a/packages/services/src/page/create.ts +++ b/packages/services/src/page/create.ts @@ -88,6 +88,12 @@ export async function createPage(args: { }) .filter((v): v is NonNullable => v !== null); if (pageComponentValues.length > 0) { + await assertWithinLimit({ + tx, + workspaceId: ctx.workspace.id, + limit: "page-components", + delta: pageComponentValues.length, + }); await tx.insert(pageComponent).values(pageComponentValues).run(); } } -- 2.51.2