Files
template-app/modules/branding/core/brand-data.ts
T

65 lines
2.8 KiB
TypeScript

// 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<typeof brandingSchema>
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<BrandView> {
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<BrandView> {
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<string, string | undefined> = { ...(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)
})
}