// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The database-only brand use cases. This file is safe to import from Convex mutations; logo // decoding and ObjectStore effects live in brand.ts and the Convex Node action workflow. import { isHexColor } from "@conusai-kit/brand" import { notFound, type TenantScope, validationFailed } from "@conusai-kit/core" import type { DataSource } from "@conusai-kit/data" import { brandingSchema, organizations } from "@conusai-kit/tenancy" import type { z } from "zod" import type { BrandView } from "../contract/view.js" type StoredBrand = z.output export interface BrandChange { displayName?: string | null | undefined primary?: string | null | undefined accent?: string | null | undefined } export const viewOf = (branding: StoredBrand | null): BrandView => ({ displayName: branding?.displayName ?? null, primary: branding?.primary ?? null, accent: branding?.accent ?? null, logoLight: branding?.logoLight ?? null, logoDark: branding?.logoDark ?? null, }) const stored = (branding: StoredBrand): StoredBrand | null => (Object.keys(branding).length === 0 ? null : branding) export async function getBrand(data: DataSource, scope: TenantScope): Promise { const org = await data.global(organizations).get(scope.orgId) if (!org) throw notFound("no such organization") return viewOf(org.branding) } export async function setBrand(data: DataSource, scope: TenantScope, change: BrandChange): Promise { const problems: Array<{ path: string; message: string }> = [] for (const field of ["primary", "accent"] as const) { const value = change[field] if (typeof value === "string" && !isHexColor(value)) problems.push({ path: field, message: "is a hex colour such as #1a1a8c" }) } if (typeof change.displayName === "string" && change.displayName.trim().length === 0) { problems.push({ path: "displayName", message: "is not empty; send null to clear it" }) } if (problems.length > 0) throw validationFailed(problems, "this brand cannot be saved") return data.unitOfWork.run(scope, async (tx) => { const org = await tx.global(organizations).get(scope.orgId) if (!org) throw notFound("no such organization") const next: Record = { ...(org.branding ?? {}) } const set = (key: keyof StoredBrand, value: string | null | undefined) => { if (value === undefined) return if (value === null) delete next[key] else next[key] = value.trim() } set("displayName", change.displayName) set("primary", change.primary) set("accent", change.accent) const branding = stored(brandingSchema.parse(next)) const updated = await tx.global(organizations).update(scope.orgId, { branding }, org.version) return viewOf(updated.branding) }) }