studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
337 lines (295 loc) • 9.3 kB
text/typescript
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],
}
) {}