UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

533 lines (506 loc) 17.1 kB
import { eq } from 'astro:db'; import { Effect, genLogger } from '../../../effect.js'; import { AstroDB, SDKCore_Generators, SDKCore_Users } from '../effect/index.js'; import { SDKCoreError, StudioCMS_SDK_Error } from '../errors.js'; import { tsDiffTracking, tsPageContent, tsPageData, tsPageDataCategories, tsPageDataTags, tsPageFolderStructure, tsPermissions, } from '../tables.js'; import type { addDatabaseEntryInsertPage, CombinedInsertContent, MultiPageInsert, tsDiffTrackingInsert, tsPageContentInsert, tsPageDataCategoriesInsert, tsPageDataInsert, tsPageDataTagsInsert, tsPageFolderInsert, tsPermissionsInsert, tsPermissionsSelect, } from '../types/index.js'; import { _ClearUnknownError, _clearLibSQLError, CacheContext, isCacheEnabled, pageDataReturn, } from '../utils.js'; import { SDKCore_CLEAR } from './clear.js'; import { SDKCore_GET } from './get.js'; import { SDKCore_UPDATE } from './update.js'; /** * SDKCore_POST provides a set of database insertion operations for StudioCMS entities, * including pages, page content, tags, categories, permissions, diff tracking, and folders. * * @remarks * This service is designed to be used within the StudioCMS SDK core, leveraging effectful * operations and dependency injection for database access and cache management. * * @example * ```typescript * const sdkPost = new SDKCore_POST(); * sdkPost.databaseEntry.pages(pageData, pageContent); * ``` * * @module studiocms/sdk/SDKCore/modules/post * * @dependencies * - AstroDB.Default * - SDKCore_CLEAR.Default * - SDKCore_Users.Default * - SDKCore_UPDATE.Default * - SDKCore_Generators.Default * - SDKCore_GET.Default * * @effect * Provides effectful methods for: * - Inserting single and multiple pages, tags, categories, permissions, folders, and diff tracking entries. * - Handling errors from the database layer and providing clear error messages. * - Managing cache and updating folder structures after insertions. * * @throws {StudioCMS_SDK_Error} * Throws when database operations fail or when attempting to insert duplicate permissions. */ export class SDKCore_POST extends Effect.Service<SDKCore_POST>()( 'studiocms/sdk/SDKCore/modules/post', { dependencies: [ AstroDB.Default, SDKCore_CLEAR.Default, SDKCore_Users.Default, SDKCore_UPDATE.Default, SDKCore_Generators.Default, SDKCore_GET.Default, ], effect: genLogger('studiocms/sdk/SDKCore/modules/post/effect')(function* () { const [dbService, CLEAR, UPDATE, GET, { generateRandomIDNumber }, { pages }] = yield* Effect.all([ AstroDB, SDKCore_CLEAR, SDKCore_UPDATE, SDKCore_GET, SDKCore_Generators, CacheContext, ]); const POST = { databaseEntry: { /** * Insert a new page into the database. * * @param pageData - The data to insert into the page data table. * @param pageContent - The data to insert into the page content table. * @returns A promise that resolves to the inserted page data and page content. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the page. */ pages: (pageData: tsPageDataInsert, pageContent: CombinedInsertContent) => Effect.gen(function* () { const newContentID = pageData.id || crypto.randomUUID().toString(); const { id = newContentID, title, slug, description, authorId = null, package: packageName = 'studiocms', contentLang = 'default', heroImage = '', showOnNav = false, showAuthor = false, showContributors = false, categories = [], tags = [], contributorIds = [], draft = false, parentFolder = null, } = pageData; const stringified = { categories: categories || [], tags: tags || [], contributorIds: contributorIds || [], }; const contentData = { id: crypto.randomUUID().toString(), contentId: newContentID, contentLang: contentLang, content: pageContent.content || '', }; const NOW = new Date(); const newPageData = yield* dbService.execute((db) => db .insert(tsPageData) .values({ id, title, slug, description, authorId, contentLang, heroImage, showAuthor, showContributors, showOnNav, draft, parentFolder, package: packageName, publishedAt: NOW, updatedAt: NOW, ...stringified, }) .returning({ id: tsPageData.id }) ); const newPageContent = yield* dbService.execute((db) => db.insert(tsPageContent).values(contentData).returning({ id: tsPageContent.id }) ); return { pageData: newPageData, pageContent: newPageContent, } as addDatabaseEntryInsertPage; }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntry.pages', cause), }) ), /** * Inserts new page content into the database. * * @param pageContent - The data to insert into the page content table. * @returns A promise that resolves to the inserted page content. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the page content. */ pageContent: dbService.makeQuery((ex, pageContent: tsPageContentInsert) => ex((db) => db .insert(tsPageContent) .values({ id: pageContent.id || crypto.randomUUID().toString(), contentId: pageContent.contentId, contentLang: pageContent.contentLang || 'default', content: pageContent.content || '', }) .returning({ id: tsPageContent.id }) ).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntry.pageContent', cause), }) ) ), /** * Inserts a new tag into the database. * * @param tag - The data to insert into the page data tags table. * @returns A promise that resolves to the inserted tag. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the tag. */ tags: (tag: tsPageDataTagsInsert) => Effect.gen(function* () { const id = tag.id || (yield* generateRandomIDNumber(9)); return yield* dbService.execute((db) => db .insert(tsPageDataTags) .values({ id, name: tag.name, description: tag.description, slug: tag.slug, meta: JSON.stringify(tag.meta), }) .returning({ id: tsPageDataTags.id }) ); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntry.tags', cause), }) ), /** * Inserts a new category into the database. * * @param category - The data to insert into the page data categories table. * @returns A promise that resolves to the inserted category. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the category. */ categories: (category: tsPageDataCategoriesInsert) => Effect.gen(function* () { const id = category.id || (yield* generateRandomIDNumber(9)); return yield* dbService.execute((db) => db .insert(tsPageDataCategories) .values({ id, name: category.name, description: category.description, slug: category.slug, meta: JSON.stringify(category.meta), }) .returning({ id: tsPageDataCategories.id }) ); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntry.tags', cause), }) ), /** * Inserts a new permission into the database. * * @param userId - The ID of the user to assign the rank to. * @param rank - The rank to assign to the user. * @returns A promise that resolves to the inserted permission. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the permission. */ permissions: (userId: string, rank: tsPermissionsSelect['rank']) => Effect.gen(function* () { const userExists = yield* dbService.execute((db) => db.select().from(tsPermissions).where(eq(tsPermissions.user, userId)).get() ); if (userExists) { return yield* _clearLibSQLError( 'POST.databaseEntry.permissions', 'User already is already assigned a rank, please update the existing rank instead.' ); } return yield* dbService.execute((db) => db .insert(tsPermissions) .values({ user: userId, rank: rank, }) .returning({ user: tsPermissions.user, rank: tsPermissions.rank }) ); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntry.permissions', cause), }) ), /** * Inserts a new diff tracking entry into the database. * * @param diff - The data to insert into the diff tracking table. * @returns A promise that resolves to the inserted diff tracking entry. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the diff tracking entry. */ diffTracking: dbService.makeQuery((ex, diff: tsDiffTrackingInsert) => ex((db) => db .insert(tsDiffTracking) .values({ id: diff.id || crypto.randomUUID().toString(), userId: diff.userId, pageId: diff.pageId, diff: diff.diff || '', timestamp: diff.timestamp || new Date(), pageContentStart: diff.pageContentStart, pageMetaData: JSON.stringify(diff.pageMetaData || {}), }) .returning() ).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntry.diffTracking', cause), }) ) ), /** * Inserts a new folder into the database. * * @param folder - The data to insert into the page folder structure table. * @returns A promise that resolves to the inserted folder. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the folder. */ folder: dbService.makeQuery((ex, folder: tsPageFolderInsert) => ex((db) => db .insert(tsPageFolderStructure) .values({ id: folder.id || crypto.randomUUID().toString(), name: folder.name, parent: folder.parent || null, }) .returning() .get() ).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntry.folder', cause), }) ) ), }, databaseEntries: { /** * Inserts multiple tags into the database. * * @param data - The data to insert into the page data tags table. * @returns A promise that resolves to the inserted tags. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the tags. */ tags: (data: tsPageDataTagsInsert[]) => Effect.gen(function* () { const entries: tsPageDataTagsInsert[] = []; for (const item of data) { const id = item.id || (yield* generateRandomIDNumber(9)); entries.push({ id, name: item.name, slug: item.slug, description: item.description, meta: JSON.stringify(item.meta), }); } return yield* dbService.execute((db) => db.insert(tsPageDataTags).values(entries).returning({ id: tsPageDataTags.id }) ); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntries.tags', cause), }) ), /** * Inserts multiple categories into the database. * * @param data - The data to insert into the page data categories table. * @returns A promise that resolves to the inserted categories. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the categories. */ categories: (data: tsPageDataCategoriesInsert[]) => Effect.gen(function* () { const entries: tsPageDataCategoriesInsert[] = []; for (const item of data) { const id = item.id || (yield* generateRandomIDNumber(9)); entries.push({ id, name: item.name, slug: item.slug, description: item.description, meta: JSON.stringify(item.meta), parent: null, }); } return yield* dbService.execute((db) => db .insert(tsPageDataCategories) .values(entries) .returning({ id: tsPageDataCategories.id }) ); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntries.categories', cause), }) ), /** * Inserts multiple permissions into the database. * * @param data - The data to insert into the permissions table. * @returns A promise that resolves to the inserted permissions. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the permissions. */ permissions: (data: tsPermissionsInsert[]) => Effect.gen(function* () { const currentPermittedUsers = yield* dbService.execute((db) => db.select().from(tsPermissions) ); for (const permission of data) { const userAlreadyExists = currentPermittedUsers.find( (user) => user.user === permission.user ); if (userAlreadyExists) { return yield* Effect.fail( new SDKCoreError({ type: 'LibSQLDatabaseError', cause: new StudioCMS_SDK_Error( `User with ID ${permission.user} already has a rank assigned. Please update the existing rank instead.` ), }) ); } } return yield* dbService.execute((db) => db .insert(tsPermissions) .values( data.map((permission) => { return { user: permission.user, rank: permission.rank, }; }) ) .returning({ user: tsPermissions.user, rank: tsPermissions.rank }) ); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('POST.databaseEntries.permissions', cause), }) ), /** * Inserts multiple pages into the database. * * @param pages - The data to insert into the page data and page content tables. * @returns A promise that resolves to the inserted pages. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the pages. */ pages: (pages: MultiPageInsert) => Effect.gen(function* () { for (const { pageData, pageContent } of pages) { yield* POST.databaseEntry.pages(pageData, pageContent); } }), }, /** * Inserts a new folder into the database and updates the cache. * @param data - The data to insert into the page folder structure table. * @returns A promise that resolves to the inserted folder. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the folder. */ folder: (data: tsPageFolderInsert) => Effect.gen(function* () { const newEntry = yield* POST.databaseEntry.folder(data); yield* CLEAR.folderList(); yield* CLEAR.folderTree(); yield* UPDATE.folderList; yield* UPDATE.folderTree; return newEntry; }), /** * Inserts a new page into the database and updates the cache. * @param data - The data to insert into the page data and page content tables. * @returns A promise that resolves to the inserted page data. * @throws {StudioCMS_SDK_Error} If an error occurs while inserting the page. */ page: (data: { pageData: tsPageDataInsert; pageContent: CombinedInsertContent }) => Effect.gen(function* () { const status = yield* isCacheEnabled; const newPage = yield* POST.databaseEntry.pages(data.pageData, data.pageContent); const pageId = newPage.pageData[0]?.id; if (!pageId) { return undefined; } const fetchedPageData = yield* GET.page.byId(pageId); if (!fetchedPageData) { return undefined; } const { data: toReturn } = fetchedPageData; if (status) { pages.set(toReturn.id, pageDataReturn(toReturn)); yield* CLEAR.folderList(); yield* CLEAR.folderTree(); } return pageDataReturn(toReturn); }).pipe( Effect.catchTags({ UnknownException: (cause) => _ClearUnknownError('POST.page', cause), }) ), }; return POST; }), } ) {}