UNPKG

@oberoncms/core

Version:

OberonCMS is a cloud deployable CMS written in typescript based on the Puck visual editor

385 lines (332 loc) 10.7 kB
import { z } from "zod" import { Data } from "@measured/puck" import { Route } from "next" import type { ComponentConfig, Config, DefaultComponentProps, } from "@measured/puck" import type { AdapterUser, Adapter as AuthAdapter } from "@auth/core/adapters" import type { StreamResponseChunk } from "@tohuhono/utils" import type { Awaitable } from "@auth/core/types" import type { NextRequest } from "next/server" export class OberonError extends Error {} export class ResponseError extends Error {} // TODO fix types // eslint-disable-next-line @typescript-eslint/no-explicit-any type Transforms = Array<(props: any) => any> export type PageData = Data export type OberonConfig = Config & { version: 1 components: Record< string, { transforms?: Transforms } > } export type OberonComponent< ComponentProps extends DefaultComponentProps = DefaultComponentProps, Transforms extends Array< (props: Record<string, unknown>) => Record<string, unknown> > = Array<(props: Record<string, unknown>) => Record<string, unknown>>, > = ComponentConfig<ComponentProps> & { transforms?: Transforms } export const clientActions = [ "edit", "preview", "users", "images", "pages", "site", "login", ] as const export const actionPaths = clientActions.map((action) => ({ path: [action], })) export type ClientAction = (typeof clientActions)[number] export type AdapterActionGroup = "all" | "users" | "images" | "pages" | "site" export type AdapterPermission = "unauthenticated" | "read" | "write" export type OberonRole = "user" | "admin" export type OberonPermissions = Record< "unauthenticated" | "user", Partial<Record<AdapterActionGroup, AdapterPermission>> > export const INITIAL_DATA = { content: [], root: { props: { title: "" } }, } satisfies Data export type MaybeOptimistic<T> = T & { pending?: boolean } /* * Pages */ export const PageSchema = z.object({ key: z .string() .regex(/^[0-9a-zA-Z_.-/]+$/, "Valid characters: 0-9 a-z A-Z -_./") .regex(/^(\/|\/[^/]+(\/[^/]+)*)$/, "Route segments cannot be empty"), data: z.object({}).passthrough(), updatedAt: z.date(), updatedBy: z.string(), }) export const AddPageSchema = PageSchema.pick({ key: true }) export const DeletePageSchema = PageSchema.pick({ key: true }) export const PublishPageSchema = PageSchema.pick({ key: true, data: true }) export const PageMetaSchema = PageSchema.pick({ key: true, updatedAt: true, updatedBy: true, }) export type OberonPage = z.infer<typeof PageSchema> & { data: PageData key: Route } // Cannot infer from zod because we need nextjs to understand key is a valid Route export type OberonPageMeta = MaybeOptimistic< z.infer<typeof PageMetaSchema> & { key: Route } > /* * Images */ export const ImageSchema = z.object({ key: z.string(), url: z.string(), size: z.number(), width: z.number().gt(0), height: z.number().gt(0), alt: z.string(), updatedAt: z.date(), updatedBy: z.string(), }) export const AddImageSchema = ImageSchema export const DeleteImageSchema = ImageSchema.pick({ key: true }) export type OberonImage = MaybeOptimistic<z.infer<typeof ImageSchema>> /* * Users */ export const UserSchema = z.object({ id: z.string(), email: z.string().email(), role: z.enum(["user", "admin"]), }) export const AddUserSchema = UserSchema.pick({ email: true, role: true }) export const ChangeRoleSchema = UserSchema.pick({ id: true, role: true }) export const DeleteUserSchema = UserSchema.pick({ id: true }) export type OberonUser = MaybeOptimistic<z.infer<typeof UserSchema>> export const roles: OberonRole[] = ["user", "admin"] as const /* * Context */ type DescriminatedContext = | { action: "edit" | "preview"; data: Data | null } | { action: "users"; data: OberonUser[] } | { action: "images"; data: OberonImage[] } | { action: "pages"; data: OberonPageMeta[] } | { action: "site"; data: OberonSiteConfig } | { action: "login" data: { callbackUrl: string; email: string; token: string } } export type OberonClientContext = DescriminatedContext & { slug: string } /* * Site */ type TransformStatus = "error" | "success" export type TransformResult = { type: "transform" key: string status: "success" | "error" } export type MigrationResult = { type: "summary" total: number } & { [key in TransformStatus]: string[] } export type TransformVersions = Record<string, number> export type PluginVersion = Pick< ReturnType<OberonPlugin>, "name" | "version" | "disabled" > export type OberonSiteConfig = MaybeOptimistic<{ version: string plugins: PluginVersion[] components: TransformVersions pendingMigrations: string[] | false }> export const SiteSchema = z.object({ version: z.number(), components: z.record(z.string(), z.number()), updatedAt: z.date(), updatedBy: z.string(), }) export type OberonSite = z.infer<typeof SiteSchema> /* * Adapter */ export type OberonCanAdapter = { getCurrentUser: () => Promise<OberonUser | null> hasPermission: (props: { user?: OberonUser | null action: AdapterActionGroup permission: AdapterPermission }) => boolean signIn: (data: { email: string }) => Promise<void> signOut: () => Promise<void> } export type OberonAuthAdapter = Required< Pick< AuthAdapter, | "createSession" | "createUser" | "createVerificationToken" | "deleteSession" | "deleteUser" | "getSessionAndUser" | "getUser" | "getUserByAccount" | "getUserByEmail" | "linkAccount" | "unlinkAccount" | "updateSession" | "updateUser" | "useVerificationToken" > > & { createUser( user: Omit<AdapterUser & { role: OberonRole }, "id">, ): Awaitable<AdapterUser & { role: OberonRole }> deleteUser: (id: OberonUser["id"]) => Promise<void> } export type OberonBaseAdapter = { addPage: (page: OberonPage) => Promise<void> addImage: (data: z.infer<typeof ImageSchema>) => Promise<void> addUser: (data: z.infer<typeof AddUserSchema>) => Promise<OberonUser> deletePage: (key: OberonPageMeta["key"]) => Promise<void> deleteImage: (key: OberonImage["key"]) => Promise<void> // TODO uploadthing deleteUser: (id: OberonUser["id"]) => Promise<void> changeRole: (data: z.infer<typeof ChangeRoleSchema>) => Promise<void> getAllImages: () => Promise<OberonImage[]> getAllPages: () => Promise<OberonPageMeta[]> getAllUsers: () => Promise<OberonUser[]> getPageData: (key: OberonPageMeta["key"]) => Promise<Data | null> getSite: () => Promise<OberonSite | undefined> updatePageData: (data: OberonPage) => Promise<void> updateSite: (data: z.infer<typeof SiteSchema>) => Promise<void> } export type OberonSendAdapter = { sendVerificationRequest: (props: { email: string token: string url: string }) => Promise<void> } export type OberonInitAdapter = { prebuild: () => Promise<void> } export type OberonDatabaseAdapter = OberonBaseAdapter & OberonAuthAdapter export type OberonPluginAdapter = OberonInitAdapter & OberonDatabaseAdapter & OberonCanAdapter & OberonSendAdapter export type OberonMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" // Currently the only handles exported are NextAuth Handlers export type OberonHandler<Params = undefined> = Params extends undefined ? { [key in OberonMethod]?: (req: NextRequest) => Promise<Response> | Response } : { [key in OberonMethod]: ( req: NextRequest, context: { params: Promise<Params> }, ) => Promise<Response> } export type OberonAdapter = { prebuild: () => Promise<void> addPage: (page: z.infer<typeof AddPageSchema>) => Promise<void> addImage: (data: OberonImage) => Promise<OberonImage[]> addUser: (data: z.infer<typeof AddUserSchema>) => Promise<OberonUser | null> deletePage: (data: z.infer<typeof DeletePageSchema>) => Promise<void> deleteImage: (key: OberonImage["key"]) => Promise<void> // TODO uploadthing deleteUser: ( data: z.infer<typeof DeleteUserSchema>, ) => Promise<Pick<OberonUser, "id"> | null> can: ( action: AdapterActionGroup, permission?: AdapterPermission, ) => Promise<boolean> changeRole: ( data: z.infer<typeof ChangeRoleSchema>, ) => Promise<Pick<OberonUser, "role" | "id"> | null> getAllImages: () => Promise<OberonImage[]> getAllPages: () => Promise<OberonPageMeta[]> getAllPaths: () => Promise<Array<{ path: string[] }>> getAllUsers: () => Promise<OberonUser[]> getConfig: () => Promise<OberonSiteConfig> getPageData: (key: OberonPageMeta["key"]) => Promise<Data | null> migrateData: () => Promise< StreamResponseChunk<TransformResult | MigrationResult> > publishPageData: ( data: z.infer<typeof PublishPageSchema>, ) => Promise<{ message: string }> signOut: () => Promise<void> signIn: (data: { email: string }) => Promise<void> } export type OberonPlugin = (adapter: OberonPluginAdapter) => { name: string version?: string disabled?: boolean handlers?: Record<string, (adapter: OberonAdapter) => OberonHandler> adapter?: Partial<OberonPluginAdapter> } export type OberonResponse<T = unknown> = Promise< | { status: "success" result: T message?: string } | { status: "error" result?: T message?: string } > export type OberonServerActions = { addPage: (page: z.infer<typeof AddPageSchema>) => OberonResponse<void> addImage: (data: OberonImage) => OberonResponse<OberonImage[]> addUser: ( data: z.infer<typeof AddUserSchema>, ) => OberonResponse<OberonUser | null> deletePage: (data: z.infer<typeof DeletePageSchema>) => OberonResponse deleteImage: (key: OberonImage["key"]) => OberonResponse deleteUser: ( data: z.infer<typeof DeleteUserSchema>, ) => OberonResponse<Pick<OberonUser, "id"> | null> can: ( action: AdapterActionGroup, permission?: AdapterPermission, ) => OberonResponse<boolean> changeRole: ( data: z.infer<typeof ChangeRoleSchema>, ) => OberonResponse<Pick<OberonUser, "role" | "id"> | null> getAllImages: () => OberonResponse<OberonImage[]> getAllPages: () => OberonResponse<OberonPageMeta[]> getAllPaths: () => OberonResponse<Array<{ path: string[] }>> getAllUsers: () => OberonResponse<OberonUser[]> getConfig: () => OberonResponse<OberonSiteConfig> getPageData: (key: OberonPageMeta["key"]) => OberonResponse<Data | null> migrateData: () => OberonResponse< StreamResponseChunk<TransformResult | MigrationResult> > publishPageData: (data: z.infer<typeof PublishPageSchema>) => OberonResponse signIn: (data: { email: string }) => OberonResponse signOut: () => OberonResponse }