UNPKG

studiocms

Version:

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

337 lines (295 loc) 9.3 kB
import { Effect } from '../../../effect.js'; import { SDKCoreError, StudioCMS_SDK_Error } from '../errors.js'; import { tsPageFolderStructure } from '../tables.js'; import type { FolderListItem, FolderNode, tsPageFolderSelect } from '../types/index.js'; import { AstroDB } from './db.js'; /** * Service class for managing and manipulating folder trees in the StudioCMS SDK. * * Provides methods to: * - Build a folder tree structure from raw folder data. * - Find nodes and paths within the folder tree by name or ID. * - Add new pages to the folder tree. * - Retrieve folder structures and lists from the database. * * @remarks * All methods are effectful and return `Effect.Effect` instances for error handling and composability. * * @example * ```typescript * const folderTreeService = SDKCore_FolderTree; * const tree = yield* folderTreeService.buildFolderTree; * ``` * * @see FolderNode * @see SDKCoreError * @see Effect */ export class SDKCore_FolderTree extends Effect.Service<SDKCore_FolderTree>()( 'studiocms/sdk/SDKCore_FolderTree', { effect: Effect.gen(function* () { const dbService = yield* AstroDB; /** * Builds a folder structure from the provided folder data. * * @param folders - An array of folder data to build the folder structure from. * @returns An array of folder nodes representing the folder structure. */ const generateFolderTree = ( folders: tsPageFolderSelect[] ): Effect.Effect<FolderNode[], SDKCoreError, never> => Effect.try({ try: () => { const folderMap: Record<string, FolderNode> = {}; for (const folder of folders) { folderMap[folder.id] = { id: folder.id, name: folder.name, pageData: null, page: false, children: [], }; } const rootFolders: FolderNode[] = []; for (const folder of folders) { const childFolder = folderMap[folder.id]; if (!childFolder) continue; if (folder.parent === null) { rootFolders.push(childFolder); } else { const parentFolder = folderMap[folder.parent]; if (parentFolder) { parentFolder.children.push(childFolder); } } } return rootFolders; }, catch: (error) => new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error(`GenerateFolderTree Error: ${error}`), }), }); /** * Finds the full path to a node based on its URL. * @param tree - The root of the folder tree. * @param path - The URL path to locate. * @returns The full path as an array of node names. */ const getFullPath = ( tree: FolderNode[], path: string[] ): Effect.Effect<string[], SDKCoreError, never> => Effect.try({ try: () => { const result: string[] = []; function helper(nodes: FolderNode[], pathParts: string[]): boolean { if (pathParts.length === 0) return false; const [current, ...rest] = pathParts; for (const node of nodes) { if (node.name === current) { result.push(node.name); if (rest.length === 0 || helper(node.children, rest)) { return true; } result.pop(); // Backtrack if not found } } return false; } helper(tree, path); return result; }, catch: (error) => new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error(`getFullPath Error: ${error}`), }), }); /** * Finds a node in the tree that matches the given URL path. * @param tree - The root of the folder tree. * @param path - The URL path to locate. * @returns The matching node or null if not found. */ const findNodeByPath = ( tree: FolderNode[], path: string[] ): Effect.Effect<FolderNode | null, SDKCoreError, never> => Effect.try({ try: () => { function _findNodeByPath(tree: FolderNode[], path: string[]): FolderNode | null { if (path.length === 0) return null; const [current, ...rest] = path; for (const node of tree) { if (node.name === current) { if (rest.length === 0) return node; return _findNodeByPath(node.children, rest); } } return null; } return _findNodeByPath(tree, path); }, catch: (error) => new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error(`FindNodeByPath Error: ${error}`), }), }); /** * Finds all nodes along the path to a given URL. * @param tree - The root of the folder tree. * @param path - The URL path to locate. * @returns The nodes along the path. */ const findNodesAlongPath = ( tree: FolderNode[], path: string[] ): Effect.Effect<FolderNode[], SDKCoreError, never> => Effect.try({ try: () => { const result: FolderNode[] = []; function helper(nodes: FolderNode[], pathParts: string[]): boolean { if (pathParts.length === 0) return false; const [current, ...rest] = pathParts; for (const node of nodes) { if (node.name === current) { result.push(node); if (rest.length === 0 || helper(node.children, rest)) { return true; } result.pop(); // Backtrack if not found } } return false; } helper(tree, path); return result; }, catch: (error) => new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error(`FindNodesAlongPath Error: ${error}`), }), }); /** * Finds all nodes along the path to a specific node by its ID. * @param tree - The root of the folder tree. * @param id - The ID of the target node. * @returns An array of nodes along the path or an empty array if the node is not found. */ const findNodesAlongPathToId = (tree: FolderNode[], id: string) => Effect.try({ try: () => { const path: FolderNode[] = []; function helper(nodes: FolderNode[], targetId: string): boolean { for (const node of nodes) { path.push(node); // Add the current node to the path if (node.id === targetId) { return true; // Target found, stop recursion } if (helper(node.children, targetId)) { return true; // Target found in descendants, propagate success } path.pop(); // Backtrack if target is not found in this branch } return false; // Target not found in this branch } helper(tree, id); return path; }, catch: (error) => new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error(`FindNodesAlongPathToId Error: ${error}`), }), }); /** * Finds a node by its ID in the tree. * @param id - The ID of the node to find. * @returns The node or null if not found. */ const findNodeById = (tree: FolderNode[], id: string) => Effect.try({ try: () => { function _findNodeById(tree: FolderNode[], id: string): FolderNode | null { for (const node of tree) { if (node.id === id) { return node; } const found = _findNodeById(node.children, id); if (found) { return found; } } return null; } return _findNodeById(tree, id); }, catch: (error) => new SDKCoreError({ type: 'UNKNOWN', cause: new StudioCMS_SDK_Error(`findNodeById Error: ${error}`), }), }); /** * Adds a new page to the folder tree. * @param tree - The root of the folder tree. * @param folderId - The ID of the parent folder. * @param newPage - The new page to add. * @returns The updated folder tree */ const addPageToFolderTree = (tree: FolderNode[], folderId: string, newPage: FolderNode) => Effect.gen(function* () { const parentFolder = yield* findNodeById(tree, folderId); if (!parentFolder) { tree.push(newPage); return tree; } parentFolder.children.push(newPage); return tree; }); /** * Gets the folder structure from the database. * * @returns A promise that resolves to an array of folder nodes representing the folder structure. */ const buildFolderTree = Effect.gen(function* () { const currentFolders = yield* dbService.execute((db) => db.select().from(tsPageFolderStructure) ); return yield* generateFolderTree(currentFolders); }); /** * Gets the available folders from the database. * * @returns A promise that resolves to an array of folder list items. */ const getAvailableFolders = Effect.gen(function* () { const folders: FolderListItem[] = []; const currentFolders = yield* dbService.execute((db) => db.select().from(tsPageFolderStructure) ); for (const current of currentFolders) { folders.push(current); } return folders; }); return { generateFolderTree, getFullPath, findNodeByPath, findNodesAlongPath, findNodesAlongPathToId, findNodeById, addPageToFolderTree, buildFolderTree, getAvailableFolders, }; }), dependencies: [AstroDB.Default], } ) {}