import { page, pageComponent, selectPageSchema, } from "@openstatus/db/src/schema"; import { emitAudit } from "../audit"; import { requireScope } from "../auth"; import { type ServiceContext, withTransaction } from "../context"; import { LimitExceededError } from "../errors"; import { assertWithinLimit } from "../limits"; import type { Page } from "../types"; import { assertAccessTypeAllowed, assertSlugAvailable, validateMonitorIdsActive, } from "./internal"; import { CreatePageInput, NewPageInput } from "./schemas"; /** Full create — mirrors legacy `pageRouter.create` (insertPageSchema input). */ export async function createPage(args: { ctx: ServiceContext; input: CreatePageInput; }): Promise { const { ctx } = args; requireScope(ctx, "write"); const input = CreatePageInput.parse(args.input); if (input.customTheme && !ctx.workspace.limits["custom-theme"]) { throw new LimitExceededError("custom-theme", 0); } if ( !ctx.workspace.limits.i18n && (input.defaultLocale !== "en" || input.locales?.some((locale) => locale !== "en")) ) { throw new LimitExceededError("i18n", 0); } return withTransaction(ctx, async (tx) => { await assertWithinLimit({ tx, workspaceId: ctx.workspace.id, limit: "status-pages", }); await assertSlugAvailable({ tx, slug: input.slug }); assertAccessTypeAllowed(ctx.workspace, { accessType: input.accessType ?? "public", passwordProtected: input.passwordProtected ?? null, allowedIpRanges: input.allowedIpRanges ?? null, allowIndex: input.allowIndex, }); const { monitors, workspaceId: _ws, id: _id, configuration, ...pageProps } = input; const monitorIds = monitors?.map((m) => m.monitorId) ?? []; const row = await tx .insert(page) .values({ workspaceId: ctx.workspace.id, // `page.configuration` is a drizzle `text("…", { mode: "json" })` // column — drizzle serialises objects automatically. Calling // `JSON.stringify` first would double-encode and persist a raw // JSON string, breaking downstream reads that expect an object. configuration, ...pageProps, authEmailDomains: pageProps.authEmailDomains?.join(","), allowedIpRanges: pageProps.allowedIpRanges?.join(","), }) .returning() .get(); if (monitorIds.length > 0) { const validMonitors = await validateMonitorIdsActive({ tx, workspaceId: ctx.workspace.id, monitorIds, }); const monitorMap = new Map(validMonitors.map((m) => [m.id, m])); const pageComponentValues = (monitors ?? []) .map(({ monitorId }, index) => { const m = monitorMap.get(monitorId); if (!m || !m.workspaceId) return null; return { workspaceId: m.workspaceId, pageId: row.id, type: "monitor" as const, monitorId, name: m.externalName || m.name, order: index, groupId: null, groupOrder: 0, }; }) .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(); } } await emitAudit(tx, ctx, { action: "page.create", entityType: "page", entityId: row.id, after: row, metadata: { slug: row.slug }, }); // `selectPageSchema.parse` normalises the drizzle row into the // `Page` shape callers expect: `authEmailDomains` / `allowedIpRanges` // go from the raw comma-joined string (drizzle-inferred) to the // `string[]` the `selectPageSchema` defines. Previously this used // `row as unknown as Page`, which hid the drift. return selectPageSchema.parse(row); }); } /** Minimal create — matches the dashboard onboarding `new` shape. */ export async function newPage(args: { ctx: ServiceContext; input: NewPageInput; }): Promise { const { ctx } = args; requireScope(ctx, "write"); const input = NewPageInput.parse(args.input); return withTransaction(ctx, async (tx) => { await assertWithinLimit({ tx, workspaceId: ctx.workspace.id, limit: "status-pages", }); await assertSlugAvailable({ tx, slug: input.slug }); const defaultConfiguration = { type: "absolute", value: "requests", uptime: true, theme: "default-rounded", }; const row = await tx .insert(page) .values({ workspaceId: ctx.workspace.id, title: input.title, slug: input.slug, description: input.description ?? "", icon: input.icon ?? "", legacyPage: false, configuration: defaultConfiguration, customDomain: "", allowIndex: true, }) .returning() .get(); await emitAudit(tx, ctx, { action: "page.create", entityType: "page", entityId: row.id, after: row, metadata: { slug: row.slug, source: "new" }, }); // `selectPageSchema.parse` normalises the drizzle row into the // `Page` shape callers expect: `authEmailDomains` / `allowedIpRanges` // go from the raw comma-joined string (drizzle-inferred) to the // `string[]` the `selectPageSchema` defines. Previously this used // `row as unknown as Page`, which hid the drift. return selectPageSchema.parse(row); }); }