UNPKG

studiocms

Version:

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

1,247 lines (1,093 loc) 43 kB
import { asc, eq, or } from 'astro:db'; import config from 'studiocms:config'; import { CMSNotificationSettingsId, GhostUserDefaults, versionCacheLifetime, } from '../../../consts.js'; import { Effect, genLogger } from '../../../effect.js'; import { FolderListMapID, FolderTreeMapID, PageFolderTreeMapID, SiteConfigMapID, StudioCMSPkgId, VersionMapID, } from '../consts.js'; import { AstroDB, GetVersionFromNPM, SDKCore_Collectors, SDKCore_FolderTree, SDKCore_Users, } from '../effect/index.js'; import { SDKCoreError, StudioCMS_SDK_Error } from '../errors.js'; import { tsDiffTracking, tsEmailVerificationTokens, tsNotificationSettings, tsOAuthAccounts, tsPageContent, tsPageData, tsPageDataCategories, tsPageDataTags, tsPageFolderStructure, tsPermissions, tsSessionTable, tsUsers, } from '../tables.js'; import type { CombinedPageData, CombinedUserData, FolderNode, MetaOnlyPageData, MetaOnlyPageDataCacheObject, PageDataCacheObject, PaginateInput, } from '../types/index.js'; import { _ClearUnknownError, _clearLibSQLError, CacheContext, convertCombinedPageDataToMetaOnly, filterPagesByDraftAndIndex, folderListReturn, folderTreeReturn, isCacheEnabled, isCacheExpired, pageDataReturn, siteConfigReturn, versionReturn, } from '../utils.js'; import { SDKCore_CONFIG } from './config.js'; /** * The `SDKCore_GET` service provides a comprehensive set of data retrieval methods for the StudioCMS SDK. * It encapsulates database queries, caching logic, and transformation utilities for core CMS entities such as pages, users, folders, permissions, and site configuration. * * @remarks * - All methods are wrapped in `Effect` for composable error handling and dependency injection. * - Caching is used for performance optimization, with cache invalidation and fallback to database queries. * - Pagination, filtering, and meta-only retrieval are supported for page queries. * * @example * ```typescript * const pages = yield* GET.pages(); * const user = yield* GET.users.byId("user-id"); * const folderTree = yield* GET.folderTree(); * ``` * * @service studiocms/sdk/SDKCore/modules/get * @dependencies * - AstroDB.Default * - SDKCore_FolderTree.Default * - GetVersionFromNPM.Default * - SDKCore_Users.Default * - SDKCore_Collectors.Default * * @effect * Provides the following main query groups: * - `databaseTable`: Direct access to raw database tables. * - `permissionsLists`: Retrieve users by permission rank. * - `users`: Retrieve user data by various identifiers. * - `folder`, `folderTree`, `folderList`, `pageFolderTree`: Folder and tree structure queries. * - `siteConfig`, `latestVersion`: Site configuration and version info. * - `page`: Retrieve page data by ID or slug. * - `folderPages`, `packagePages`, `pages`: Retrieve pages by folder, package, or all pages. * * @throws {SDKCoreError} If a database or unknown error occurs during retrieval. */ export class SDKCore_GET extends Effect.Service<SDKCore_GET>()( 'studiocms/sdk/SDKCore/modules/get', { dependencies: [ AstroDB.Default, SDKCore_FolderTree.Default, GetVersionFromNPM.Default, SDKCore_Users.Default, SDKCore_Collectors.Default, SDKCore_CONFIG.Default, ], effect: genLogger('studiocms/sdk/SDKCore/modules/get/effect')(function* () { const [ dbService, { FolderList, pages, folderTree, version, siteConfig, pageFolderTree }, { buildFolderTree, getAvailableFolders, addPageToFolderTree }, getVersionFromNPM, { combineRanks, verifyRank }, { collectUserData, collectPageData }, { siteConfig: sdkSiteConfig }, ] = yield* Effect.all([ AstroDB, CacheContext, SDKCore_FolderTree, GetVersionFromNPM, SDKCore_Users, SDKCore_Collectors, SDKCore_CONFIG, ]); /** * Validates the pagination input by ensuring that `limit` and `offset` are non-negative values. * If `limit` is zero, it sets a default value of 10. * Returns the validated (and possibly modified) pagination input. * If validation fails, yields an `SDKCoreError` with a descriptive message. * * @template P - Type extending `PaginateInput`. * @param paginate - The pagination input to validate. * @returns The validated pagination input or fails with an error if validation fails. */ const validatePagination = Effect.fn(function* <P extends PaginateInput>(paginate: P) { if (paginate.limit < 0 || paginate.offset < 0) { return yield* Effect.fail( new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error( 'Pagination limit and offset must be non-negative values' ), }) ); } if (paginate.limit === 0) { paginate.limit = 10; // Default value } return paginate; }); /** * Retrieves a folder from the database by its ID or name. * * This function queries the `tsPageFolderStructure` table for a folder whose `id` or `name` * matches the provided `idOrName` parameter. If no matching folder is found, it fails with a * `LibSQLDatabaseError`. * * @param idOrName - The ID or name of the folder to retrieve. * @returns The folder data if found. * @throws {SDKCoreError} If the folder is not found in the database. */ const getFolderByNameOrId = Effect.fn(function* (idOrName: string) { const data = yield* dbService.execute((db) => db .select() .from(tsPageFolderStructure) .where( or(eq(tsPageFolderStructure.id, idOrName), eq(tsPageFolderStructure.name, idOrName)) ) .get() ); if (!data) { return yield* Effect.fail( new SDKCoreError({ type: 'LibSQLDatabaseError', cause: new StudioCMS_SDK_Error(`Folder not found in Database: ${idOrName}`), }) ); } return data; }); function _getPackagesPages( packageName: string, tree?: FolderNode[] ): Effect.Effect<CombinedPageData[], SDKCoreError, never>; function _getPackagesPages( packageName: string, tree?: FolderNode[], metaOnly?: boolean ): Effect.Effect<MetaOnlyPageData[], SDKCoreError, never>; /** * Retrieves all pages associated with a given package name, optionally using a provided folder tree and returning only metadata if specified. * * @param packageName - The name of the package to fetch pages for. * @param tree - Optional. A pre-built folder tree to use for organizing pages. If not provided, the folder tree will be built automatically. * @param metaOnly - Optional. If true, returns only metadata for each page; otherwise, returns combined page data. Defaults to false. * @returns An Effect that resolves to an array of page data, either as `MetaOnlyPageData[]` or `CombinedPageData[]` depending on `metaOnly`. */ function _getPackagesPages(packageName: string, tree?: FolderNode[], metaOnly = false) { return Effect.gen(function* () { const pagesRaw = yield* dbService.execute((db) => db.select().from(tsPageData).where(eq(tsPageData.package, packageName)) ); const folders = tree || (yield* buildFolderTree); const pages = []; for (const page of pagesRaw) { const data = yield* collectPageData(page, folders, metaOnly); pages.push(data); } if (metaOnly) { return pages as MetaOnlyPageData[]; } return pages as CombinedPageData[]; }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('GET.packagePages', cause), }) ); } function _getPageById( id: string ): Effect.Effect<PageDataCacheObject | undefined, SDKCoreError, never>; function _getPageById( id: string, metaOnly?: boolean ): Effect.Effect<MetaOnlyPageDataCacheObject | undefined, SDKCoreError, never>; /** * Retrieves a page by its ID, with an option to return only metadata. * * @param id - The ID of the page to retrieve. * @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`. * @returns An `Effect` that yields the page data or metadata, or `undefined` if no page is found. * * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ function _getPageById(id: string, metaOnly = false) { const getPage = (id: string, tree?: FolderNode[]) => Effect.gen(function* () { const page = yield* dbService.execute((db) => db.select().from(tsPageData).where(eq(tsPageData.id, id)).get() ); if (!page) return undefined; const folders = tree || (yield* buildFolderTree); return yield* collectPageData(page, folders); }); return Effect.gen(function* () { const status = yield* isCacheEnabled; if (!status) { const page = yield* getPage(id); if (!page) { return undefined; } const pageData = pageDataReturn(page); return metaOnly ? convertCombinedPageDataToMetaOnly(pageData) : pageData; } const { data: tree } = yield* GET.folderTree(); const cachedPage = pages.get(id); if (!cachedPage || isCacheExpired(cachedPage)) { const page = yield* getPage(id, tree); if (!page) { return undefined; } const returnPage = pageDataReturn(page); pages.set(id, returnPage); return metaOnly ? convertCombinedPageDataToMetaOnly(returnPage) : returnPage; } // Return the cached page return metaOnly ? convertCombinedPageDataToMetaOnly(cachedPage) : cachedPage; }).pipe( Effect.catchTags({ UnknownException: (cause: unknown) => _clearLibSQLError('GET.page.byId', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.page.byId', cause), }) ); } function _getPageBySlug( slug: string ): Effect.Effect<PageDataCacheObject | undefined, SDKCoreError, never>; function _getPageBySlug( slug: string, metaOnly?: boolean ): Effect.Effect<MetaOnlyPageDataCacheObject | undefined, SDKCoreError, never>; /** * Retrieves a page by its slug, with an option to return only metadata. * * @param slug - The slug of the page to retrieve. * @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`. * @returns An `Effect` that yields the page data or metadata, or `undefined` if no page is found. * * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ function _getPageBySlug(slug: string, metaOnly = false) { const getPage = (iSlug: string, tree?: FolderNode[]) => Effect.gen(function* () { const page = yield* dbService.execute((db) => db.select().from(tsPageData).where(eq(tsPageData.slug, iSlug)).get() ); if (!page) return undefined; const folders = tree || (yield* buildFolderTree); return yield* collectPageData(page, folders); }); return Effect.gen(function* () { const status = yield* isCacheEnabled; if (!status) { const page = yield* getPage(slug); if (!page) { return undefined; } const pageData = pageDataReturn(page); return metaOnly ? convertCombinedPageDataToMetaOnly(pageData) : pageData; } const { data: tree } = yield* GET.folderTree(); // Retrieve the cached page const cachedPage = Array.from(pages.values()).find((page) => page.data.slug === slug); if (!cachedPage || isCacheExpired(cachedPage)) { const page = yield* getPage(slug, tree); if (!page) { return undefined; } const pageData = pageDataReturn(page); pages.set(page.id, pageData); return metaOnly ? convertCombinedPageDataToMetaOnly(pageData) : pageData; } // Return the cached page return metaOnly ? convertCombinedPageDataToMetaOnly(cachedPage) : cachedPage; }).pipe( Effect.catchTags({ UnknownException: (cause) => _clearLibSQLError('GET.page.bySlug', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.page.bySlug', cause), }) ); } function _getAllPages( includeDrafts?: boolean, hideDefaultIndex?: boolean, metaOnly?: false, paginate?: PaginateInput ): Effect.Effect<PageDataCacheObject[], SDKCoreError, never>; function _getAllPages( includeDrafts?: boolean, hideDefaultIndex?: boolean, metaOnly?: true, paginate?: PaginateInput ): Effect.Effect<MetaOnlyPageDataCacheObject[], SDKCoreError, never>; /** * Retrieves all pages from the database or cache, with options for including drafts, * hiding the default index page, returning only metadata, and paginating results. * * - If caching is enabled, attempts to return cached pages and updates expired cache entries. * - If caching is disabled, fetches pages directly from the database. * - Supports filtering by draft status and default index visibility. * - Can return either full page data or metadata only. * - Supports pagination via the `paginate` parameter. * * @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`. * @param hideDefaultIndex - Whether to exclude the default index page from the results. Defaults to `false`. * @param metaOnly - Whether to return only metadata for each page. Defaults to `false`. * @param paginate - Optional pagination input specifying `limit` and `offset`. * @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`. */ function _getAllPages( includeDrafts = false, hideDefaultIndex = false, metaOnly = false, paginate?: PaginateInput ) { const getPages = ( includeDrafts = false, hideDefaultIndex = false, tree?: FolderNode[], paginate?: PaginateInput ) => Effect.gen(function* () { let pagesRaw: (typeof tsPageData.$inferSelect)[] = []; if (paginate) { const paginateData = yield* validatePagination(paginate); pagesRaw = yield* dbService.execute((db) => db .select() .from(tsPageData) .orderBy(asc(tsPageData.title)) .limit(paginateData.limit) .offset(paginateData.offset) ); } else { pagesRaw = yield* dbService.execute((db) => db.select().from(tsPageData).orderBy(asc(tsPageData.title)) ); } const pagesFiltered = filterPagesByDraftAndIndex( pagesRaw, includeDrafts, hideDefaultIndex ); const folders = tree || (yield* buildFolderTree); const pages = []; for (const page of pagesFiltered) { const data = yield* collectPageData(page, folders); pages.push(data); } return pages; }); return Effect.gen(function* () { const status = yield* isCacheEnabled; if (!status) { const dbPages = yield* getPages(includeDrafts, hideDefaultIndex, undefined, paginate); const data = dbPages.map((page) => pageDataReturn(page)); return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data; } const { data: tree } = yield* GET.folderTree(); if (pages.size === 0) { const newData = yield* getPages(includeDrafts, hideDefaultIndex, tree); // Loop through the updated data and store it in the cache for (const data of newData) { pages.set(data.id, pageDataReturn(data)); } const data = newData.map((data) => pageDataReturn(data)); if (paginate) { const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title)); const paginatedData = sortedData.slice( paginate.offset, paginate.offset + paginate.limit ); return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData; } // Transform and return the data return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data; } const cacheMap = Array.from(pages.values()); for (const item of cacheMap) { if (isCacheExpired(item)) { const updated = yield* GET.page.byId(item.data.id); if (!updated) { pages.delete(item.data.id); continue; } pages.set(updated.data.id, updated); } } const data = Array.from(pages.values()); if (paginate) { const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title)); const paginatedData = sortedData.slice( paginate.offset, paginate.offset + paginate.limit ); return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData; } return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data; }).pipe( Effect.catchTags({ UnknownException: (cause) => _clearLibSQLError('GET.pages', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.pages', cause), }) ); } function _folderPages( idOrName: string, includeDrafts?: boolean, hideDefaultIndex?: boolean, metaOnly?: false, paginate?: PaginateInput ): Effect.Effect<PageDataCacheObject[], SDKCoreError, never>; function _folderPages( idOrName: string, includeDrafts?: boolean, hideDefaultIndex?: boolean, metaOnly?: true, paginate?: PaginateInput ): Effect.Effect<MetaOnlyPageDataCacheObject[], SDKCoreError, never>; /** * Retrieves pages within a specified folder by its ID or name, supporting various filtering and pagination options. * * This function handles both cached and non-cached scenarios, fetching pages from the database or cache as appropriate. * It supports filtering drafts, hiding default index pages, returning only metadata, and paginating results. * * @param idOrName - The ID or name of the folder to retrieve pages from. * @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`. * @param hideDefaultIndex - Whether to exclude default index pages from the results. Defaults to `false`. * @param metaOnly - If `true`, returns only metadata for each page. Defaults to `false`. * @param paginate - Optional pagination input specifying `limit` and `offset`. * @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`. * * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ function _folderPages( idOrName: string, includeDrafts = false, hideDefaultIndex = false, metaOnly = false, paginate?: PaginateInput ) { const getAllPages = ( includeDrafts = false, hideDefaultIndex = false, tree?: FolderNode[], paginate?: PaginateInput ) => Effect.gen(function* () { let pagesRaw: (typeof tsPageData.$inferSelect)[] = []; if (paginate) { const paginateData = yield* validatePagination(paginate); pagesRaw = yield* dbService.execute((db) => db .select() .from(tsPageData) .orderBy(asc(tsPageData.title)) .limit(paginateData.limit) .offset(paginateData.offset) ); } else { pagesRaw = yield* dbService.execute((db) => db.select().from(tsPageData).orderBy(asc(tsPageData.title)) ); } const pagesFiltered = filterPagesByDraftAndIndex( pagesRaw, includeDrafts, hideDefaultIndex ); const folders = tree || (yield* buildFolderTree); const pages = []; for (const page of pagesFiltered) { const data = yield* collectPageData(page, folders); pages.push(data); } return pages; }); const getPages = ( id: string, includeDrafts = false, hideDefaultIndex = false, tree?: FolderNode[] ) => Effect.gen(function* () { const pagesRaw = yield* dbService.execute((db) => db .select() .from(tsPageData) .where(eq(tsPageData.parentFolder, id)) .orderBy(asc(tsPageData.title)) ); const pagesFiltered = filterPagesByDraftAndIndex( pagesRaw, includeDrafts, hideDefaultIndex ); const folders = tree || (yield* buildFolderTree); const pages = []; for (const page of pagesFiltered) { pages.push(yield* collectPageData(page, folders)); } return pages; }); return Effect.gen(function* () { const status = yield* isCacheEnabled; const folderData = yield* getFolderByNameOrId(idOrName); if (!status) { const dbPages = yield* getPages(folderData.id, includeDrafts, hideDefaultIndex); const data = dbPages.map((page) => pageDataReturn(page)); return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data; } const { data: tree } = yield* GET.folderTree(); if (pages.size === 0) { const updatedData = yield* getAllPages(includeDrafts, hideDefaultIndex, tree); for (const data of updatedData) { pages.set(data.id, pageDataReturn(data)); } const data = updatedData .filter(({ parentFolder }) => parentFolder === folderData.id) .map((data) => pageDataReturn(data)); if (paginate) { const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title)); const paginatedData = sortedData.slice( paginate.offset, paginate.offset + paginate.limit ); return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData; } // Transform and return the data return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data; } const cacheMap = Array.from(pages.values()); for (const item of cacheMap) { if (isCacheExpired(item)) { const updated = yield* GET.page.byId(item.data.id); if (!updated) { pages.delete(item.data.id); continue; } pages.set(updated.data.id, updated); } } const data = Array.from(pages.values()).filter( ({ data: { parentFolder } }) => parentFolder === folderData.id ); if (paginate) { const sortedData = data.sort((a, b) => a.data.title.localeCompare(b.data.title)); const paginatedData = sortedData.slice( paginate.offset, paginate.offset + paginate.limit ); return metaOnly ? convertCombinedPageDataToMetaOnly(paginatedData) : paginatedData; } return metaOnly ? convertCombinedPageDataToMetaOnly(data) : data; }).pipe( Effect.catchTags({ UnknownException: (cause) => _clearLibSQLError('GET.pages', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.pages', cause), }) ); } /** * Retrieves the list of permissions for users with the specified rank. * * This function queries the database for all permitted users and existing users, * then verifies and returns the permissions associated with the given rank. * Handles database errors by invoking a custom error handler. * * @param rank - The rank of users to retrieve permissions for. Must be one of 'owner', 'admin', 'editor', or 'visitor'. * @returns An Effect that resolves to the verified permissions for the specified rank, or handles database errors. */ const getPermissionsByRank = (rank: 'owner' | 'admin' | 'editor' | 'visitor') => Effect.gen(function* () { const currentPermittedUsers = yield* dbService.execute((db) => db.select().from(tsPermissions) ); const existingUsers = yield* dbService.execute((db) => db.select().from(tsUsers)); return yield* verifyRank(existingUsers, currentPermittedUsers, rank); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError(`GET.permissionsLists.${rank}s`, cause), }) ); const GET = { databaseTable: { users: () => dbService.execute((db) => db.select().from(tsUsers)), oAuthAccounts: () => dbService.execute((db) => db.select().from(tsOAuthAccounts)), sessionTable: () => dbService.execute((db) => db.select().from(tsSessionTable)), permissions: () => dbService.execute((db) => db.select().from(tsPermissions)), pageData: () => dbService.execute((db) => db.select().from(tsPageData)), pageDataTags: () => dbService.execute((db) => db.select().from(tsPageDataTags)), pageDataCategories: () => dbService.execute((db) => db.select().from(tsPageDataCategories)), pageContent: () => dbService.execute((db) => db.select().from(tsPageContent)), diffTracking: () => dbService.execute((db) => db.select().from(tsDiffTracking)), pageFolderStructure: () => dbService.execute((db) => db.select().from(tsPageFolderStructure)), notificationSettings: () => dbService.execute((db) => db .select() .from(tsNotificationSettings) .where(eq(tsNotificationSettings.id, CMSNotificationSettingsId)) .get() ), emailVerificationTokens: () => dbService.execute((db) => db.select().from(tsEmailVerificationTokens)), }, permissionsLists: { /** * Retrieves all permissions for users in the database. * * @returns A promise that resolves to an array of combined rank data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the permissions. */ all: () => Effect.gen(function* () { const currentPermittedUsers = yield* dbService.execute((db) => db.select().from(tsPermissions) ); const existingUsers = yield* dbService.execute((db) => db.select().from(tsUsers)); const owners = yield* verifyRank(existingUsers, currentPermittedUsers, 'owner'); const admins = yield* verifyRank(existingUsers, currentPermittedUsers, 'admin'); const editors = yield* verifyRank(existingUsers, currentPermittedUsers, 'editor'); const visitors = yield* verifyRank(existingUsers, currentPermittedUsers, 'visitor'); return [ ...(yield* combineRanks('owner', owners)), ...(yield* combineRanks('admin', admins)), ...(yield* combineRanks('editor', editors)), ...(yield* combineRanks('visitor', visitors)), ]; }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('GET.permissionsLists.all', cause), }) ), /** * Retrieves all owners in the database. * * @returns A promise that resolves to an array of combined rank data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the owners. */ owners: () => getPermissionsByRank('owner'), /** * Retrieves all admins in the database. * * @returns A promise that resolves to an array of combined rank data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the admins. */ admins: () => getPermissionsByRank('admin'), /** * Retrieves all editors in the database. * * @returns A promise that resolves to an array of combined rank data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the editors. */ editors: () => getPermissionsByRank('editor'), /** * Retrieves all visitors in the database. * * @returns A promise that resolves to an array of combined rank data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the visitors. */ visitors: () => getPermissionsByRank('visitor'), }, users: { /** * Retrieves all users from the database. * * @returns A promise that resolves to an array of combined user data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the users. */ all: () => Effect.gen(function* () { const combinedUserData: CombinedUserData[] = []; const users = yield* dbService.execute((db) => db.select().from(tsUsers)); for (const user of users) { if (user.id === GhostUserDefaults.id) { continue; } const UserData = yield* collectUserData(user); combinedUserData.push(UserData); } return combinedUserData; }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('GET.users.all', cause), }) ), /** * Retrieves a user by ID. * * @param id - The ID of the user to retrieve. * @returns A promise that resolves to the user data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the user. */ byId: (id: string) => Effect.gen(function* () { const user = yield* dbService.execute((db) => db.select().from(tsUsers).where(eq(tsUsers.id, id)).get() ); if (!user) return undefined; return yield* collectUserData(user); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('GET.users.byId', cause), }) ), /** * Retrieves a user by username. * * @param username - The username of the user to retrieve. * @returns A promise that resolves to the user data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the user. */ byUsername: (username: string) => Effect.gen(function* () { const user = yield* dbService.execute((db) => db.select().from(tsUsers).where(eq(tsUsers.username, username)).get() ); if (!user) return undefined; return yield* collectUserData(user); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('GET.users.byId', cause), }) ), /** * Retrieves a user by email. * * @param email - The email of the user to retrieve. * @returns A promise that resolves to the user data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the user. */ byEmail: (email: string) => Effect.gen(function* () { const user = yield* dbService.execute((db) => db.select().from(tsUsers).where(eq(tsUsers.email, email)).get() ); if (!user) return undefined; return yield* collectUserData(user); }).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('GET.users.byId', cause), }) ), }, /** * Retrieves a folder by ID. * * @param id - The ID of the folder to retrieve. * @returns A promise that resolves to the folder data. * @throws {StudioCMS_SDK_Error} If an error occurs while getting the folder. */ folder: dbService.makeQuery((ex, id: string) => ex((db) => db.select().from(tsPageFolderStructure).where(eq(tsPageFolderStructure.id, id)).get() ).pipe( Effect.catchTags({ LibSQLClientError: (cause) => _clearLibSQLError('GET.folder', cause), }) ) ), /** * Retrieves the site configuration, with caching support. * If caching is enabled, it checks the cache for an existing site config. * If the cache is expired or not available, it fetches the site config from the database * and updates the cache. * @returns An `Effect` that yields the site config. * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ siteConfig: () => Effect.gen(function* () { const status = yield* isCacheEnabled; if (config.dbStartPage) { return undefined; } if (!status) { const newConfig = yield* sdkSiteConfig.get(); if (!newConfig) { return yield* Effect.fail( new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error('Site config not found in database'), }) ); } return siteConfigReturn(newConfig.data); } const currentSiteConfig = siteConfig.get(SiteConfigMapID); if (!currentSiteConfig || isCacheExpired(currentSiteConfig)) { const newConfig = yield* sdkSiteConfig.get(); if (!newConfig) { return yield* Effect.fail( new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error('Site config not found in database'), }) ); } const returnConfig = siteConfigReturn(newConfig.data); siteConfig.set(SiteConfigMapID, returnConfig); return returnConfig; } return currentSiteConfig; }).pipe( Effect.catchTags({ UnknownException: (cause) => _clearLibSQLError('GET.siteConfig', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.siteConfig', cause), }) ), /** * Retrieves the folder tree structure, with caching support. * If caching is enabled, it checks the cache for an existing folder tree. * If the cache is expired or not available, it fetches the folder tree from the database * and updates the cache. * @returns An `Effect` that yields the folder tree. * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ folderTree: () => Effect.gen(function* () { const status = yield* isCacheEnabled; if (!status) { return folderTreeReturn(yield* buildFolderTree); } const tree = folderTree.get(FolderTreeMapID); if (!tree || isCacheExpired(tree)) { const newFolderTree = yield* buildFolderTree; const returnable = folderTreeReturn(newFolderTree); folderTree.set(FolderTreeMapID, returnable); return returnable; } return tree; }).pipe( Effect.catchTags({ UnknownException: (cause) => _clearLibSQLError('GET.folderTree', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.folderTree', cause), }) ), /** * Retrieves the folder tree structure, with caching support. * If caching is enabled, it checks the cache for an existing folder tree. * If the cache is expired or not available, it fetches the folder tree from the database * and updates the cache. * @returns An `Effect` that yields the folder tree. * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ pageFolderTree: (hideDefaultIndex = false) => Effect.gen(function* () { const status = yield* isCacheEnabled; if (!status) { const folderTree = yield* buildFolderTree; const pages = yield* GET.pages(true, hideDefaultIndex); for (const { data: page } of pages) { if (page.parentFolder) { yield* addPageToFolderTree(folderTree, page.parentFolder, { id: page.id, name: page.title, page: true, pageData: page, children: [], }); } else { folderTree.push({ id: page.id, name: page.title, page: true, pageData: page, children: [], }); } } return folderTreeReturn(folderTree); } const tree = pageFolderTree.get(PageFolderTreeMapID); if (!tree || isCacheExpired(tree)) { const newFolderTree = yield* buildFolderTree; const pages = yield* GET.pages(true, hideDefaultIndex); for (const { data: page } of pages) { if (page.parentFolder) { yield* addPageToFolderTree(newFolderTree, page.parentFolder, { id: page.id, name: page.title, page: true, pageData: page, children: [], }); } else { newFolderTree.push({ id: page.id, name: page.title, page: true, pageData: page, children: [], }); } } pageFolderTree.set(PageFolderTreeMapID, folderTreeReturn(newFolderTree)); return folderTreeReturn(newFolderTree); } return tree; }).pipe( Effect.catchTags({ UnknownException: (cause) => _clearLibSQLError('GET.pageFolderTree', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.pageFolderTree', cause), }) ), /** * Retrieves a list of available folders, with caching support. * If caching is enabled, it checks the cache for an existing folder list. * If the cache is expired or not available, it fetches the folder list from the database * and updates the cache. * @returns An `Effect` that yields the folder list. * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ folderList: () => Effect.gen(function* () { const status = yield* isCacheEnabled; if (!status) { const folderList = yield* getAvailableFolders; return folderListReturn(folderList); } const list = FolderList.get(FolderListMapID); if (!list || isCacheExpired(list)) { const folderList = yield* getAvailableFolders; FolderList.set(FolderListMapID, folderListReturn(folderList)); return folderListReturn(folderList); } return list; }).pipe( Effect.catchTags({ UnknownException: (cause) => _clearLibSQLError('GET.folderList', cause), LibSQLClientError: (cause) => _clearLibSQLError('GET.folderList', cause), }) ), /** * Retrieves the latest version of StudioCMS from NPM, with caching support. * @returns An `Effect` that yields the latest version of StudioCMS. * @throws UnknownException - If an unknown error occurs during retrieval. */ latestVersion: () => Effect.gen(function* () { const status = yield* isCacheEnabled; if (!status) { const version = yield* getVersionFromNPM.get(StudioCMSPkgId); return versionReturn(version); } const latestVersion = version.get(VersionMapID); if (!latestVersion || isCacheExpired(latestVersion, versionCacheLifetime)) { const newVersion = yield* getVersionFromNPM.get(StudioCMSPkgId); const latestVersion = versionReturn(newVersion); version.set(VersionMapID, latestVersion); return latestVersion; } return latestVersion; }).pipe( Effect.catchTags({ UnknownException: (cause) => _ClearUnknownError('GET.latestVersion', cause), }) ), page: { /** * Retrieves a page by its ID, with an option to return only metadata. * * @param id - The ID of the page to retrieve. * @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`. * @returns An `Effect` that yields the page data or metadata, depending on `metaOnly`. * * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ byId: _getPageById, /** * Retrieves a page by its slug, with an option to return only metadata. * * @param slug - The slug of the page to retrieve. * @param metaOnly - If `true`, returns only metadata for the page. Defaults to `false`. * @returns An `Effect` that yields the page data or metadata, depending on `metaOnly`. * * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ bySlug: _getPageBySlug, }, /** * Retrieves pages within a specified folder by its ID or name, supporting various filtering and pagination options. * * This function handles both cached and non-cached scenarios, fetching pages from the database or cache as appropriate. * It supports filtering drafts, hiding default index pages, returning only metadata, and paginating results. * * @param idOrName - The ID or name of the folder to retrieve pages from. * @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`. * @param hideDefaultIndex - Whether to exclude default index pages from the results. Defaults to `false`. * @param metaOnly - If `true`, returns only metadata for each page. Defaults to `false`. * @param paginate - Optional pagination input specifying `limit` and `offset`. * @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`. * * @throws UnknownException - If an unknown error occurs during retrieval. * @throws LibSQLDatabaseError - If a database error occurs during retrieval. */ folderPages: _folderPages, /** * Retrieves all pages associated with a given package name, optionally using a provided folder tree and returning only metadata if specified. * * @param packageName - The name of the package to fetch pages for. * @param tree - Optional. A pre-built folder tree to use for organizing pages. If not provided, the folder tree will be built automatically. * @param metaOnly - Optional. If true, returns only metadata for each page; otherwise, returns combined page data. Defaults to false. * @returns An Effect that resolves to an array of page data, either as `MetaOnlyPageData[]` or `CombinedPageData[]` depending on `metaOnly`. */ packagePages: _getPackagesPages, /** * Retrieves all pages from the database or cache, with options for including drafts, * hiding the default index page, returning only metadata, and paginating results. * * - If caching is enabled, attempts to return cached pages and updates expired cache entries. * - If caching is disabled, fetches pages directly from the database. * - Supports filtering by draft status and default index visibility. * - Can return either full page data or metadata only. * - Supports pagination via the `paginate` parameter. * * @param includeDrafts - Whether to include draft pages in the results. Defaults to `false`. * @param hideDefaultIndex - Whether to exclude the default index page from the results. Defaults to `false`. * @param metaOnly - Whether to return only metadata for each page. Defaults to `false`. * @param paginate - Optional pagination input specifying `limit` and `offset`. * @returns An `Effect` that yields an array of page data or metadata, depending on `metaOnly`. */ pages: _getAllPages, }; return GET; }), } ) {}