UNPKG

studiocms

Version:

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

294 lines (266 loc) 9.41 kB
import { eq, inArray } from 'astro:db'; import { type Cause, Effect } from '../../../effect.js'; import { SDKCoreError, StudioCMS_SDK_Error } from '../errors.js'; import { tsOAuthAccounts, tsPageContent, tsPageDataCategories, tsPageDataTags, tsPermissions, tsUsers, } from '../tables.js'; import type { CombinedPageData, CombinedUserData, FolderNode, MetaOnlyPageData, PageDataReturnType, tsPageDataSelect, tsUsersSelect, } from '../types/index.js'; import { AstroDB } from './db.js'; import { SDKCore_FolderTree } from './foldertree.js'; import { SDKCore_Parsers } from './parsers.js'; /** * Effect service providing core data collectors for StudioCMS SDK. * * @remarks * This service includes methods for collecting categories, tags, page data, and user data, * aggregating information from the database and related services. All methods return * `Effect.Effect` instances for composable error handling and async operations. * * @example * ```typescript * const collectors = yield* SDKCore_Collectors; * const categories = yield* collectors.collectCategories([1, 2, 3]); * ``` * * @service SDKCore_Collectors * @dependencies AstroDB, SDKCore_FolderTree, SDKCore_Parsers * * @method collectCategories * Collects categories by their IDs. * @param categoryIds - Array of category IDs. * @returns Effect resolving to array of categories. * @throws SDKCoreError if database error occurs. * * @method collectTags * Collects tags by their IDs. * @param tagIds - Array of tag IDs. * @returns Effect resolving to array of tags. * @throws SDKCoreError if database error occurs. * * @method collectPageData * Collects and combines all data related to a page, including categories, tags, contributors, author, and content. * @param page - Page data object. * @param tree - FolderNode tree. * @param metaOnly - If true, returns only metadata. * @returns Effect resolving to combined page data or meta-only data. * @throws SDKCoreError if database or unknown error occurs. * * @method collectUserData * Collects user data including OAuth and permissions. * @param user - User data object. * @returns Effect resolving to combined user data. * @throws SDKCoreError if database error occurs. */ export class SDKCore_Collectors extends Effect.Service<SDKCore_Collectors>()('SDKCore_Collectors', { effect: Effect.gen(function* () { const dbService = yield* AstroDB; const folderTreeService = yield* SDKCore_FolderTree; const parseService = yield* SDKCore_Parsers; /** * Collects categories based on the provided category IDs. * * @param categoryIds - An array of category IDs to collect. * @returns A promise that resolves to an array of collected categories. * @throws {StudioCMS_SDK_Error} If there is an error while collecting categories. */ const collectCategories = (categoryIds: number[]) => Effect.gen(function* () { const categories: CombinedPageData['categories'] = []; if (categoryIds.length > 0) { const resItem = yield* dbService.execute((db) => db .select() .from(tsPageDataCategories) .where(inArray(tsPageDataCategories.id, categoryIds)) ); if (resItem) categories.push(...resItem); } return categories as CombinedPageData['categories']; }); /** * Collects tags based on the provided tag IDs. * * @param tagIds - An array of tag IDs to collect. * @returns A promise that resolves to an array of tags. * @throws {StudioCMS_SDK_Error} If an error occurs while fetching the tags. */ const collectTags = (tagIds: number[]) => Effect.gen(function* () { const tags: CombinedPageData['tags'] = []; if (tagIds.length > 0) { const resItem = yield* dbService.execute((db) => db.select().from(tsPageDataTags).where(inArray(tsPageDataTags.id, tagIds)) ); if (resItem) tags.push(...resItem); } return tags as CombinedPageData['tags']; }); /** * Transforms the collected page data to include only metadata. * * @param data - The page data to transform. * @returns A promise that resolves to the transformed page data. * @throws {UnknownException} If an error occurs during transformation. */ const transformPageDataToMetaOnly = <T extends CombinedPageData[] | CombinedPageData>( data: T ): Effect.Effect<PageDataReturnType<T>, Cause.UnknownException, never> => Effect.try(() => { if (Array.isArray(data)) { return data.map( ({ defaultContent, multiLangContent, ...rest }) => rest ) as PageDataReturnType<T>; } const { defaultContent: _dump1, multiLangContent: _dump2, ...rest } = data as CombinedPageData; return rest as PageDataReturnType<T>; }); function collectPageData( page: tsPageDataSelect, tree: FolderNode[] ): Effect.Effect<CombinedPageData, SDKCoreError, never>; function collectPageData( page: tsPageDataSelect, tree: FolderNode[], metaOnly: boolean ): Effect.Effect<MetaOnlyPageData, SDKCoreError, never>; /** * Collects and combines various data related to a page. * * @param page - The page data to collect additional information for. * @param tree - The FolderNode tree * @param metaOnly - Only return the metadata and not the pageContent * @returns A promise that resolves to the combined page data. * @throws {StudioCMS_SDK_Error} If an error occurs while collecting page data. */ function collectPageData(page: tsPageDataSelect, tree: FolderNode[], metaOnly = false) { return Effect.gen(function* () { const categoryIds = yield* parseService.parseIdNumberArray(page.categories || []); const tagIds = yield* parseService.parseIdNumberArray(page.tags || []); const contributorIds = yield* parseService.parseIdStringArray(page.contributorIds || []); const categories = yield* collectCategories(categoryIds); const tags = yield* collectTags(tagIds); const authorData = yield* dbService.execute((db) => db .select() .from(tsUsers) .where(eq(tsUsers.id, page.authorId || '')) .get() ); let contributorsData: tsUsersSelect[]; if (contributorIds.length) { contributorsData = yield* dbService.execute((db) => db.select().from(tsUsers).where(inArray(tsUsers.id, contributorIds)) ); } else { contributorsData = []; } let multiLanguageContentData: | { id: string; contentLang: string; contentId: string; content: string | null; }[] | undefined; if (!metaOnly) { multiLanguageContentData = yield* dbService.execute((db) => db.select().from(tsPageContent).where(eq(tsPageContent.contentId, page.id)) ); } const defaultLanguageContentData = multiLanguageContentData?.find( (content) => content.contentLang === page.contentLang ); const safeSlug = page.slug === 'index' ? '/' : `/${page.slug}`; let urlRoute = safeSlug; if (page.parentFolder) { const urlParts = yield* folderTreeService.findNodesAlongPathToId(tree, page.parentFolder); urlRoute = `/${urlParts.map((part) => part.name).join('/')}${safeSlug}`; } const returnData = { ...page, urlRoute, categories, tags, contributorIds, authorData, contributorsData, multiLangContent: multiLanguageContentData, defaultContent: defaultLanguageContentData, } as CombinedPageData; if (metaOnly) { return yield* transformPageDataToMetaOnly(returnData); } return returnData; }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => Effect.fail( new SDKCoreError({ type: 'LibSQLDatabaseError', cause: new StudioCMS_SDK_Error(`Error collecting page data: ${cause}`), }) ), UnknownException: (cause) => Effect.fail( new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error(`Error collecting page data: ${cause}`), }) ), }) ); } /** * Collects user data by fetching OAuth data and permission data from the database. * * @param user - The user object containing user information. * @returns A promise that resolves to a CombinedUserData object containing the user data, OAuth data, and permissions data. * @throws {StudioCMS_SDK_Error} If an error occurs while collecting user data. */ const collectUserData = ( user: tsUsersSelect ): Effect.Effect<CombinedUserData, SDKCoreError, never> => Effect.gen(function* () { const oAuthData = yield* dbService.execute((db) => db.select().from(tsOAuthAccounts).where(eq(tsOAuthAccounts.userId, user.id)) ); const permissionsData = yield* dbService.execute((db) => db.select().from(tsPermissions).where(eq(tsPermissions.user, user.id)).get() ); return { ...user, oAuthData, permissionsData, } as CombinedUserData; }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => Effect.fail( new SDKCoreError({ type: 'LibSQLDatabaseError', cause: new StudioCMS_SDK_Error(`Error collecting page data: ${cause}`), }) ), }) ); return { collectCategories, collectTags, collectPageData, collectUserData }; }), dependencies: [AstroDB.Default, SDKCore_FolderTree.Default, SDKCore_Parsers.Default], }) {}