import fs from 'fs/promises' import path from 'path' import { compact, sortBy } from 'lodash' import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' import { generateDeterministicUuid } from './snippets.browser' import { SNIPPETS_DIR } from './snippets.constants' type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T export const SnippetSchema = z.object({ id: z.string().uuid(), inserted_at: z.string().default(() => new Date().toISOString()), updated_at: z.string().default(() => new Date().toISOString()), type: z.literal('sql'), name: z.string(), description: z.string().optional(), favorite: z.boolean().default(false), content: z.object({ sql: z.string(), content_id: z.string(), schema_version: z.literal('1.0'), }), visibility: z.union([ z.literal('user'), z.literal('project'), z.literal('org'), z.literal('public'), ]), project_id: z.number().default(1), folder_id: z.string().nullable().default(null), owner_id: z.number().default(1), owner: z .object({ id: z.number(), username: z.string(), }) .default({ id: 1, username: 'johndoe' }), updated_by: z .object({ id: z.number(), username: z.string(), }) .default({ id: 1, username: 'johndoe' }), }) export const FolderSchema = z.object({ id: z.string(), name: z.string(), owner_id: z.number().default(1), parent_id: z.string().nullable(), project_id: z.number().default(1), }) export type Snippet = z.infer export type Folder = z.infer export type FilesystemEntry = { id: string name: string type: 'file' | 'folder' folderId: string | null content?: string // Only for files createdAt: Date } const buildSnippet = ( filename: string, content: string, folderId: string | null, createdAt: Date ) => { const snippet: Snippet = { id: generateDeterministicUuid([folderId, `${filename}.sql`]), inserted_at: createdAt.toISOString(), updated_at: createdAt.toISOString(), type: 'sql', name: filename.replace('.sql', ''), description: '', favorite: false, content: { sql: content, content_id: uuidv4(), schema_version: '1.0', }, visibility: 'user', project_id: 1, folder_id: folderId, owner_id: 1, owner: { id: 1, username: 'johndoe' }, updated_by: { id: 1, username: 'johndoe' }, } return snippet } const buildFolder = (name: string) => { const folder: Folder = { id: generateDeterministicUuid([name]), name: name, owner_id: 1, parent_id: null, project_id: 1, } return folder } const sanitizeName = (name: string): string => { // Remove path traversal sequences and normalize const sanitized = path.basename(name) if (sanitized !== name || name.includes('\0')) { throw new Error('Invalid name: path traversal or null bytes detected') } return sanitized } /** * Gets a complete snapshot of the filesystem structure including files and folders * @returns An array of files and folders with their metadata */ export async function getFilesystemEntries(): Promise { if (SNIPPETS_DIR === '') { throw new Error( 'SNIPPETS_MANAGEMENT_FOLDER env var is not set. Please set it to use snippets properly.' ) } // Ensure the snippets directory exists try { await fs.access(SNIPPETS_DIR) } catch { await fs.mkdir(SNIPPETS_DIR, { recursive: true }) } const entries: FilesystemEntry[] = [] const readEntriesRecursively = async ( dirPath: string, folderName: string | null ): Promise => { const items = await fs.readdir(dirPath, { withFileTypes: true }) const folderId = folderName ? generateDeterministicUuid([folderName]) : null for (const item of items) { const itemPath = path.join(dirPath, item.name) if (item.isDirectory()) { // if the folder entry is under another folder, skip it. Subdirectories are not supported. if (folderName) { continue } const stats = await fs.stat(itemPath) // Add folder entry entries.push({ id: generateDeterministicUuid([folderId, item.name]), name: item.name, type: 'folder', // Folders are always at root level in this implementation folderId: null, createdAt: stats.birthtime, }) await readEntriesRecursively(itemPath, item.name) } else if (item.isFile() && item.name.endsWith('.sql')) { const [content, stats] = await Promise.all([ fs.readFile(itemPath, 'utf-8'), fs.stat(itemPath), ]) const snippetName = item.name.replace('.sql', '') entries.push({ id: generateDeterministicUuid([folderId, `${snippetName}.sql`]), name: snippetName, type: 'file', folderId: folderId, content: content, createdAt: stats.birthtime, }) } } } await readEntriesRecursively(SNIPPETS_DIR, null) return entries } export const getSnippet = async (snippetId: string) => { const entries = await getFilesystemEntries() const foundSnippet = entries.find((e) => e.type === 'file' && e.id === snippetId) if (!foundSnippet) { throw new Error(`Snippet with id ${snippetId} not found`) } return buildSnippet( foundSnippet.name, foundSnippet.content || '', foundSnippet.folderId, foundSnippet.createdAt ) } /** * Gets a filtered paginated list of snippets based on the provided criteria */ export const getSnippets = async ({ searchTerm, limit, cursor, sort, sortOrder, folderId, }: { searchTerm?: string limit?: number cursor?: string sortOrder?: 'asc' | 'desc' sort?: 'name' | 'inserted_at' folderId?: string | null }): Promise<{ cursor: string | undefined; snippets: Snippet[] }> => { // Normalize and set default values const normalizedSearchTerm = searchTerm?.trim() ?? '' const normalizedLimit = limit ?? 100 const normalizedSort = sort ?? 'inserted_at' const normalizedSortOrder = sortOrder ?? 'desc' const normalizedCursor = cursor ?? undefined const normalizedFolderId = folderId ?? null // Validate inputs if (normalizedLimit <= 0) { throw new Error('Limit must be a positive number') } if (normalizedLimit > 1000) { throw new Error('Limit cannot exceed 1000') } const entries = await getFilesystemEntries() const files = entries.filter( (entry): entry is FilesystemEntry & { type: 'file'; content: string } => entry.type === 'file' && entry.content !== undefined && entry.content !== null ) // Filter snippets based on search term or folder let filteredSnippets = files if (normalizedSearchTerm) { // When searching, look across all folders and support case-insensitive search filteredSnippets = files.filter((file) => file.name.toLowerCase().includes(normalizedSearchTerm.toLowerCase()) ) } else { // Filter by specific folder or root (null) filteredSnippets = files.filter((file) => file.folderId === normalizedFolderId) } // Sort snippets const sortedSnippets = sortBy(filteredSnippets, (snippet) => { if (normalizedSort === 'inserted_at') { return snippet.createdAt.getTime() } return snippet.name.toLowerCase() // Case-insensitive name sorting }) if (normalizedSortOrder === 'desc') { sortedSnippets.reverse() } // Apply cursor-based pagination let paginatedSnippets = sortedSnippets if (normalizedCursor) { const cursorIndex = sortedSnippets.findIndex((s) => s.id === normalizedCursor) if (cursorIndex !== -1) { paginatedSnippets = sortedSnippets.slice(cursorIndex + 1) } // If cursor not found, return all snippets (graceful degradation) } // Apply limit and determine next cursor let nextCursor: string | undefined = undefined let finalSnippets = paginatedSnippets if (normalizedLimit && paginatedSnippets.length > normalizedLimit) { finalSnippets = paginatedSnippets.slice(0, normalizedLimit) nextCursor = finalSnippets[finalSnippets.length - 1].id } return { cursor: nextCursor, snippets: finalSnippets.map((file) => buildSnippet(file.name, file.content, file.folderId, file.createdAt) ), } } /** * Saves a snippet to the filesystem */ export async function saveSnippet(snippet: Snippet): Promise { const entries = await getFilesystemEntries() const existingSnippet = entries.find((entry) => entry.id === snippet.id && entry.type === 'file') if (existingSnippet) { throw new Error(`Snippet with id ${snippet.id} already exists`) } // check if the folder exists if (snippet.folder_id !== null) { const existingFolder = entries.find( (entry) => entry.id === snippet.folder_id && entry.type === 'folder' ) if (existingFolder === undefined) { throw new Error(`Folder with id ${snippet.folder_id} not found`) } } const snippetName = sanitizeName(snippet.name) const content = snippet.content.sql || '' const folderId = snippet.folder_id || null const folder = entries.find((f) => f.id === folderId && f.type === 'folder') const folderPath = folder ? path.join(SNIPPETS_DIR, folder.name) : SNIPPETS_DIR const filePath = path.join(folderPath, `${snippetName}.sql`) await fs.writeFile(filePath, content || '', 'utf-8') const stats = await fs.stat(filePath) const result = buildSnippet(snippetName, content, snippet.folder_id, stats.birthtime) return result } /** * Deletes a snippet from the filesystem */ export async function deleteSnippet(id: string): Promise { const entries = await getFilesystemEntries() const found = entries.find((entry) => entry.id === id && entry.type === 'file') if (!found) { throw new Error(`Snippet with id ${id} not found`) } const filename = `${found.name}.sql` const currentFolder = entries.find((f) => f.id === found.folderId && f.type === 'folder') const paths = compact([SNIPPETS_DIR, currentFolder?.name, filename]) const filePath = path.join(...paths) try { await fs.unlink(filePath) } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error } } } /** * Updates a snippet in the filesystem. It also handles renaming and moving. */ export async function updateSnippet(id: string, updates: DeepPartial): Promise { const entries = await getFilesystemEntries() const foundSnippet = entries .filter( (entry): entry is FilesystemEntry & { type: 'file'; content: string } => entry.type === 'file' ) .find((s) => s.id === id) if (!foundSnippet) { throw new Error(`Snippet with id ${id} not found`) } const newId = generateDeterministicUuid([ updates.folder_id !== undefined ? updates.folder_id : foundSnippet.folderId, `${updates.name ?? foundSnippet.name}.sql`, ]) const snippetAtTargetLocation = entries.find( (entry) => entry.id === newId && entry.type === 'file' ) if (snippetAtTargetLocation && snippetAtTargetLocation.id !== foundSnippet.id) { throw new Error( `Snippet named "${updates.name ?? foundSnippet.name}" already exists in the specified folder` ) } const snippet = buildSnippet( foundSnippet.name, foundSnippet.content || '', foundSnippet.folderId, foundSnippet.createdAt ) // it's easier to delete the old file first and then recreate a new one await deleteSnippet(snippet.id) const updatedSnippet = await saveSnippet({ name: updates.name ?? snippet.name, content: updates.content ?? snippet.content, // folder_id can be null folder_id: updates.folder_id !== undefined ? updates.folder_id : snippet.folder_id, } as Snippet) return updatedSnippet } export const getFolders = async (folderId: string | null = null): Promise => { const entries = await getFilesystemEntries() const folders = entries .filter( (entry): entry is FilesystemEntry & { type: 'folder' } => entry.type === 'folder' && entry.folderId === folderId ) .map((folder) => buildFolder(folder.name)) return folders } /** * Creates a new folder as an actual directory */ export async function createFolder(_folderName: string): Promise { const folderName = sanitizeName(_folderName) const entries = await getFilesystemEntries() const existingFolder = entries.find((folder) => folder.name === folderName) if (existingFolder) { throw new Error(`Folder with name ${folderName} already exists`) } const folderPath = path.join(SNIPPETS_DIR, folderName) await fs.mkdir(folderPath, { recursive: true }) const newFolder = buildFolder(folderName) return newFolder } /** * Deletes a folder directory from the filesystem * @throws {Error} If the folder doesn't exist */ export async function deleteFolder(id: string): Promise { const entries = await getFilesystemEntries() const folder = entries.find((f) => f.id === id && f.type === 'folder') if (!folder) { throw new Error(`Folder with id ${id} not found`) } const folderPath = path.join(SNIPPETS_DIR, folder.name) try { await fs.rm(folderPath, { recursive: true, force: true }) } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error } // If folder doesn't exist, still throw the original error throw new Error(`Folder with id ${id} not found`) } }