UNPKG

@oberoncms/core

Version:

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

369 lines (331 loc) 8.9 kB
import { revalidatePath, updateTag, unstable_cache as cache } from "next/cache" import { type Data } from "@puckeditor/core" import { streamResponse } from "@tohuhono/utils" import { version } from "../../package.json" with { type: "json" } import { AddImageSchema, AddUserSchema, ChangeRoleSchema, DeletePageSchema, DeleteUserSchema, INITIAL_DATA, AddPageSchema, PublishPageSchema, ResponseError, type AdapterActionGroup, type AdapterPermission, type OberonAdapter, type OberonUser, type OberonConfig, type MigrationResult, type TransformResult, type OberonPage, type PageData, type OberonPluginAdapter, type PluginVersion, } from "../lib/dtd" import { applyTransforms, getComponentTransformVersions, getTransforms, } from "./transforms" export function initAdapter({ config, versions, pluginAdapter: adapter, }: { config: OberonConfig pluginAdapter: OberonPluginAdapter versions: PluginVersion[] }): OberonAdapter { const isPageData = (value: unknown): value is PageData => { return ( typeof value === "object" && value !== null && "content" in value && "root" in value ) } const can: OberonAdapter["can"] = async (action, permission = "read") => { // Check unauthenticated first so we can do it outside of request context if (adapter.hasPermission({ action, permission })) { return true } const user = await adapter.getCurrentUser() return adapter.hasPermission({ user, action, permission }) } const will = async ( action: AdapterActionGroup, permission: AdapterPermission, ) => { if (await can(action, permission)) { return } throw new ResponseError("You do not have permission to perform this action") } const whoWill = async ( action: AdapterActionGroup, permission: AdapterPermission, ) => { const user = await adapter.getCurrentUser() if (user && adapter.hasPermission({ user, action, permission })) { return user } throw new ResponseError("You do not have permission to perform this action") } const getAllPagesCached = cache( async () => { const sortPages = (a: { key: string }, b: { key: string }) => { if (a.key < b.key) { return -1 } if (a.key > b.key) { return 1 } return 0 } const result = await adapter.getAllPages() const data = result.sort(sortPages) return data }, undefined, { tags: ["oberon-pages"] }, ) const getAllPathsCached = cache( async () => { const result = await adapter.getAllPages() const data = result.map((row) => ({ path: row["key"].split("/").slice(1), })) return data }, undefined, { tags: ["oberon-pages"] }, ) // TODO zod ; maybeGet const getPageDataCached = async (key: string): Promise<Data | null> => { const dataString = await adapter.getPageData(key) return dataString } const getAllUsersCached = cache( async () => { const allUsers = await adapter.getAllUsers() return allUsers || [] }, undefined, { tags: ["oberon-users"], }, ) const getAllImagesCached = cache( async () => { const allImages = await adapter.getAllImages() return allImages || [] }, undefined, { tags: ["oberon-images"], }, ) const updatePageData = async ({ key, data, updatedBy, }: Pick<OberonPage, "key" | "data" | "updatedBy">) => { await adapter.updatePageData({ key, data, updatedAt: new Date(), updatedBy, }) revalidatePath(key) updateTag("oberon-pages") } const getConfigCached = cache( async () => { const site = await adapter.getSite() const { components, transforms } = getTransforms(site?.components, config) const siteConfig = { version, plugins: versions, components, pendingMigrations: transforms && Object.keys(transforms), } if (!site) { await adapter.updateSite({ version: config.version, components: getComponentTransformVersions(config), updatedAt: new Date(), updatedBy: "system", }) } return siteConfig }, undefined, { tags: ["oberon-config"], }, ) const migrate = streamResponse< TransformResult | MigrationResult, [OberonUser] >(async function* (user: OberonUser) { const summary: MigrationResult = { type: "summary", error: [], success: [], total: 0, } const site = await adapter.getSite() const { transforms } = getTransforms(site?.components, config) if (!transforms) { return summary } const pages = await getAllPagesCached() const results = applyTransforms({ transforms, pages, getPageData: getPageDataCached, updatePageData, }) for await (const result of results) { summary[result.status].push(result.key) yield result } await adapter.updateSite({ version: config.version, components: getComponentTransformVersions(config), updatedAt: new Date(), updatedBy: user.email, }) updateTag("oberon-config") yield { ...summary, total: pages.length } }) return { getSetting: async (namespace, key) => { return adapter.getKV(namespace, key) }, prebuild: async () => { console.log("adapter prebuild") await adapter.prebuild() console.log("prebuild done") }, /* * Auth */ can, signIn: adapter.signIn, signOut: adapter.signOut, /* * Site actions */ getConfig: async () => { await will("site", "read") return await getConfigCached() }, migrateData: async () => { const user = await whoWill("site", "write") return migrate(user) }, /* * Page actions */ getAllPaths: async function () { await will("pages", "read") return getAllPathsCached() }, getAllPages: async function () { await will("pages", "read") return getAllPagesCached() }, getPageData: async function (key) { await will("pages", "read") return getPageDataCached(key) }, // TODO return value addPage: async function (data: unknown) { const user = await whoWill("pages", "write") const { key } = AddPageSchema.parse(data) await adapter.addPage({ key, data: INITIAL_DATA, updatedAt: new Date(), updatedBy: user.email, }) revalidatePath(key) updateTag("oberon-pages") }, // TODO return value deletePage: async function (data: unknown) { await will("pages", "write") const { key } = DeletePageSchema.parse(data) await adapter.deletePage(key) revalidatePath(key) updateTag("oberon-pages") }, publishPageData: async function (data: unknown) { const user = await whoWill("pages", "write") const { key, data: pageData } = PublishPageSchema.parse(data) if (!isPageData(pageData)) { throw new ResponseError("Invalid page data") } await updatePageData({ key, data: pageData, updatedBy: user.email, }) return { message: `Successfully published ${key}` } }, /* * Image actions */ getAllImages: async function () { await will("images", "read") return getAllImagesCached() }, addImage: async function (data: unknown) { await will("images", "write") const image = AddImageSchema.parse(data) await adapter.addImage(image) updateTag("oberon-images") return adapter.getAllImages() }, // TODO uploadthing deleteImage: async function (data) { await will("images", "write") updateTag("oberon-images") return adapter.deleteImage(data) }, /* * User actions */ getAllUsers: async function () { await will("users", "read") return getAllUsersCached() }, addUser: async function (data: unknown) { await will("users", "write") const { email, role } = AddUserSchema.parse(data) const { id } = await adapter.addUser({ email, role, }) updateTag("oberon-users") return { id, email, role } }, deleteUser: async function (data: unknown) { await will("users", "write") const { id } = DeleteUserSchema.parse(data) await adapter.deleteUser(id) updateTag("oberon-users") return { id } }, changeRole: async function (data: unknown) { await will("users", "write") const { role, id } = ChangeRoleSchema.parse(data) await adapter.changeRole({ role, id }) updateTag("oberon-users") return { role, id } }, } }