This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user