@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
text/typescript
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
}