feat: add typescript to file-graph and server

This commit is contained in:
Lin, Hung-Shin authored and wancat committed 2024-01-29 12:42:50 +08:00
1 parent e6abd08963
commit 3977db87a3
19 files changed
+340 -15

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
/* eslint-disable max-classes-per-file */
export class DuplicateItemError extends Error {
constructor(message) {
super(message)
this.name = 'DuplicateItemError'
}
}
export class ParentNotExistsError extends Error {
constructor(message) {
super(message)
this.name = 'ParentNotExistsError'
}
}
export class FileNotExistsError extends Error {
constructor(message) {
super(message)
this.name = 'FileNotExistsError'
}
}
+21
View File
@@ -0,0 +1,21 @@
import { basename } from './utils.js'
export default class File {
constructor({ path, type, discovered }) {
this.path = path.trimEnd('/')
this.name = basename(path)
this.type = type
this.discovered = discovered
}
isDirectory() {
return this.type === 'folder'
}
contains(file) {
if (!this.isDirectory()) return false
if (this.path === file.path) return true
const parentPath = this.path.endsWith('/') ? this.path : `${this.path}/`
return file.path.startsWith(parentPath)
}
}
+48
View File
@@ -0,0 +1,48 @@
import File from './file.js'
describe('File', () => {
let file
beforeEach(() => {
file = new File({ path: '/home/user', type: 'folder', discovered: true })
})
const filesToCompare = [
{
name: 'direct children',
path: '/home/user/file1.txt',
answer: true,
},
{
name: 'nested children',
path: '/home/user/folder1/file1.txt',
answer: true,
},
{
name: 'outer file',
path: '/etc/file1.txt',
answer: false,
},
{
name: 'fake children',
path: '/home/user2/file2.txt',
answer: false,
},
{
name: 'same',
path: '/home/user',
answer: true,
},
{
name: 'same with trailing',
path: '/home/user/',
answer: true,
},
]
filesToCompare.forEach(({ name, path, answer }) => {
test(`${name}: ${path}`, () => {
const fileToCompare = new File({ path, type: 'file', discovered: true })
expect(file.contains(fileToCompare)).toBe(answer)
})
})
})
+53
View File
@@ -0,0 +1,53 @@
import FileNode from './fileNode.js'
export default class FileGraph extends FileNode {
add(files) {
// Logic to add the file node to the graph
// You can implement the logic to find the appropriate location in the file graph
// If the file node already exists, throw an error
files.forEach((file) => {
this.addChild(file, { makeParents: true })
})
}
remove(files) {
// Logic to remove a file node from the graph
// If the file node is a folder, also remove all children
// If the file node doesn't exist, throw an error
files.forEach((file) => {
this.removeChild(file)
})
}
discover(eventFiles) {
// Logic to handle the discover event
// Overwrite the file graph with the provided files
// Remove file nodes that don't exist anymore and add new ones
const newNode = new FileNode(eventFiles[0])
eventFiles.slice(1).forEach((file) => {
newNode.addChild(file, { makeParents: true })
})
this.merge(newNode)
}
handleEvent(event) {
// eslint-disable-next-line default-case
switch (true) {
case !!event.add:
this.add(event.add)
break
case !!event.remove:
this.remove(event.remove)
break
case !!event.discover:
this.discover(event.discover)
break
// Implement other cases if needed
}
}
}
+174
View File
@@ -0,0 +1,174 @@
import FileGraph from './fileGraph.js'
import FileNode from './fileNode.js'
describe('FileGraph', () => {
let root
beforeEach(() => {
root = new FileNode({
path: '/',
type: 'folder',
discovered: true,
})
})
test('add files to graph', () => {
const fileGraph = new FileGraph(root)
const filesToAdd = [
{
path: '/home/user/file1.txt',
type: 'file',
discovered: true,
},
{
path: '/home/user/folder1',
type: 'folder',
discovered: true,
},
]
fileGraph.add(filesToAdd)
expect(fileGraph.children.length).toBe(1)
expect(fileGraph.children[0].children[0].children.length).toBe(2)
})
test('remove files from graph', () => {
const fileGraph = new FileGraph(root)
const filesToAdd = [
{
path: '/home/user/folder1',
type: 'folder',
discovered: true,
},
{
path: '/home/user/folder2',
type: 'folder',
discovered: true,
},
{
path: '/home/user/folder1/file1.txt',
type: 'file',
discovered: true,
},
{
path: '/home/user/folder2/file2.txt',
type: 'file',
discovered: true,
},
]
fileGraph.add(filesToAdd)
const filesToRemove = [
{
path: '/home/user/folder1/file1.txt',
type: 'folder',
},
{
path: '/home/user/folder2',
type: 'folder',
},
]
fileGraph.remove(filesToRemove)
expect(fileGraph.children[0].children[0].children.length).toBe(1)
expect(fileGraph.children[0].children[0].children[0].name).toBe('folder1')
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
0
)
})
test('discover', () => {
const fileGraph = new FileGraph(root)
const discoverFiles = [
{
path: '/home/user/folder1',
type: 'folder',
discovered: true,
},
{
path: '/home/user/folder1/file1.txt',
type: 'file',
discovered: true,
},
{
path: '/home/user/folder1/file2.txt',
type: 'file',
discovered: true,
},
{
path: '/home/user/folder1/file3.txt',
type: 'file',
discovered: true,
},
]
fileGraph.discover(discoverFiles)
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
3
)
})
test('discover new files', () => {
const fileGraph = new FileGraph(root)
const filesToAdd = [
{
path: '/home/user/folder1',
type: 'folder',
discovered: true,
},
{
path: '/home/user/folder2',
type: 'folder',
discovered: true,
},
{
path: '/home/user/folder1/file1.txt',
type: 'file',
discovered: true,
},
{
path: '/home/user/folder2/file2.txt',
type: 'file',
discovered: true,
},
]
fileGraph.add(filesToAdd)
const newFiles = [
{
path: '/home/user/folder1',
type: 'folder',
discovered: true,
},
{
path: '/home/user/folder1/file1.txt',
type: 'file',
discovered: true,
},
{
path: '/home/user/folder1/file2.txt',
type: 'file',
discovered: true,
},
{
path: '/home/user/folder1/file3.txt',
type: 'file',
discovered: true,
},
]
fileGraph.discover(newFiles)
expect(fileGraph.children[0].children[0].children.length).toBe(2)
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
3
)
})
})
+133
View File
@@ -0,0 +1,133 @@
import File from './file.js'
import {
DuplicateItemError,
ParentNotExistsError,
FileNotExistsError,
} from './errors.js'
import { basename, dirname } from './utils.js'
export default class FileNode extends File {
constructor(file) {
super(file)
this.children = []
if (file.children) this.children = file.children.map((f) => new FileNode(f))
}
addChild(fileInput, { makeParents } = {}) {
const file = new FileNode(fileInput)
if (file.path === this.path) {
throw new DuplicateItemError(
`File ${file.path} already exists in the tree: ${this.toString()}`
)
}
if (this.children.some((child) => child.path === file.path)) {
throw new DuplicateItemError(
`File ${file.path} already exists in the tree: ${this.toString()}`
)
}
const parentPath = dirname(file.path)
if (parentPath === this.path) {
// Direct child of this node
this.children.push(file)
} else {
// Recursively call addChild on the correct child node
const childNode = this.children.find((child) => child.contains(file))
if (childNode) {
childNode.addChild(file)
} else if (makeParents) {
const parent = new FileNode({
path: parentPath,
name: basename(parentPath),
type: 'folder',
discovered: false,
children: [file],
})
this.addChild(parent, { makeParents })
} else {
throw new ParentNotExistsError(
`Parent path not found in the tree, ${file.path}, ${this.path}`
)
}
}
}
removeChild(fileToRemove) {
const file = new FileNode(fileToRemove)
const parentPath = dirname(file.path)
if (parentPath === this.path) {
// Direct child of this node
this.children = this.children.filter((child) => child.path !== file.path)
} else {
// Recursively call removeChild on the correct child node
const childNode = this.children.find((child) => child.contains(file))
if (childNode) {
childNode.removeChild(file)
} else {
throw new FileNotExistsError(
`File ${file.path} does not exist in the tree: ${this.toString()}`
)
}
}
}
merge(fileNode) {
// Merge the children of the provided file node into this node
// If a child already exists, merge the children of that child
// If a child does not exist, add it to this node
// If this is the same node, merge the children
if (fileNode.path === this.path) {
// Update the state of this node
this.discovered = this.discovered || fileNode.discovered
fileNode.children.forEach((child) => {
const existingChild = this.children.find((c) => c.path === child.path)
if (existingChild) {
existingChild.merge(child)
} else {
this.children.push(child)
}
})
// If a child exists in this node but not in the provided node, remove it
if (fileNode.children.length > 0) {
this.children = this.children.filter((child) =>
fileNode.children.some((c) => c.path === child.path)
)
}
}
// If this contains the parent node
else if (this.contains(fileNode)) {
const childNode = this.children.find((child) => child.contains(fileNode))
// If a child node exists, merge the provided node into the child node
if (childNode) {
childNode.merge(fileNode)
}
// If a child node does not exist, add the provided node as a child
else {
this.addChild(fileNode, { makeParents: true })
}
} else {
throw new Error(
'Cannot merge file nodes with different paths',
this.path,
fileNode.path
)
}
}
toString(level = 0) {
let { name } = this
const indent = ' '.repeat(level)
name = `${indent}${name}`
if (this.isDirectory()) name += '/'
if (!this.discovered) name += `*`
const childrenString = this.children
.map((child) => child.toString(level + 1))
.join('')
return `${name}\n${childrenString}`
}
}
+277
View File
@@ -0,0 +1,277 @@
import { DuplicateItemError, ParentNotExistsError } from './errors.js'
import FileNode from './fileNode.js'
function createNodeFromFiles(files) {
const node = new FileNode(files[0])
files.slice(1).forEach((file) => node.addChild(file))
return node
}
function mergeTest(files1, files2, expectedFiles) {
const node1 = createNodeFromFiles(files1)
const node2 = createNodeFromFiles(files2)
const expectedNode = createNodeFromFiles(expectedFiles)
node1.merge(node2)
expect(node1.toString()).toBe(expectedNode.toString())
}
function removeChildTest(files, fileToRemove, expectedFiles) {
const node = createNodeFromFiles(files)
const expectedNode = createNodeFromFiles(expectedFiles)
node.removeChild(fileToRemove)
expect(node.toString()).toBe(expectedNode.toString())
}
describe('FileNode', () => {
let rootNode
beforeEach(() => {
rootNode = new FileNode({
path: '/home/rudeus',
name: 'rudeus',
type: 'folder',
discovered: true,
})
})
test('should throw error when adding duplicate item', () => {
const child = {
path: '/home/rudeus/test',
name: 'test',
type: 'folder',
}
// Adding the child for the first time
rootNode.addChild(child)
expect(rootNode.children.length).toBe(1)
// Trying to add the same child again should throw an error
expect(() => {
rootNode.addChild(child)
}).toThrow(DuplicateItemError)
})
test('should throw error when parent does not exist', () => {
const child = {
path: '/home/rudeus/alphabet/test',
name: 'test',
type: 'folder',
}
// Trying to add to a nonexistent parent should throw an error
expect(() => {
rootNode.addChild(child)
}).toThrow(ParentNotExistsError)
})
test('adds nested children to the tree', () => {
const files = [
{
path: '/home/rudeus/forgotten_scroll.txt',
name: 'forgotten_scroll.txt',
type: 'file',
discovered: 'test',
},
{
path: '/home/rudeus/test',
name: 'test',
type: 'folder',
discovered: 'test',
},
{
path: '/home/rudeus/test/a',
name: 'a',
type: 'file',
discovered: false,
},
]
files.forEach((file) => {
rootNode.addChild(file)
})
expect(rootNode.children.length).toBe(2)
expect(rootNode.children[1].children.length).toBe(1)
})
test('adds nested children to the tree with makeParents option', () => {
const file = {
path: '/home/rudeus/test/a/b/c/d/e',
name: 'a',
type: 'file',
discovered: false,
}
rootNode.addChild(file, { makeParents: true })
let currentNode = rootNode
for (let i = 0; i < 5; i += 1) {
expect(currentNode.children.length).toBe(1)
// eslint-disable-next-line prefer-destructuring
currentNode = currentNode.children[0]
}
})
test('merge-subtree', () => {
const root = {
path: '/',
type: 'folder',
}
const files1 = [
root,
{ path: '/a', type: 'file' },
{ path: '/b', type: 'folder' },
{ path: '/b/c', type: 'folder' },
{ path: '/b/d', type: 'file' },
{ path: '/b/c/f', type: 'file' },
]
const files2 = [
{ path: '/b', type: 'folder' },
{ path: '/b/c', type: 'folder' },
{ path: '/b/e', type: 'file' },
{ path: '/b/c/g', type: 'file' },
]
const expected = [
root,
{ path: '/a', type: 'file' },
{ path: '/b', type: 'folder' },
{ path: '/b/c', type: 'folder' },
{ path: '/b/e', type: 'file' },
{ path: '/b/c/g', type: 'file' },
]
mergeTest(files1, files2, expected)
})
test('merge-middle-level', () => {
// Test if the node to merge is only the middle part of the tree
// (i.e. the node to merge doesn't contains all level of children)
// The original tree should keep the children that are not in the node to merge
const root = {
path: '/',
type: 'folder',
}
const files1 = [
root,
{ path: '/a', type: 'file' },
{ path: '/b', type: 'folder' },
{ path: '/b/c', type: 'folder' },
{ path: '/b/d', type: 'file' },
{ path: '/b/c/f', type: 'file' },
]
const files2 = [
{ path: '/b', type: 'folder' },
{ path: '/b/c', type: 'folder' },
{ path: '/b/e', type: 'file' },
]
const expected = [
root,
{ path: '/a', type: 'file' },
{ path: '/b', type: 'folder' },
{ path: '/b/c', type: 'folder' },
{ path: '/b/e', type: 'file' },
{ path: '/b/c/f', type: 'file' },
]
mergeTest(files1, files2, expected)
})
test('merge-hidden', () => {
const root = {
path: '/',
type: 'folder',
}
const files1 = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/.b', type: 'file', discovered: false },
{ path: '/c', type: 'folder', discovered: true },
{ path: '/c/.d', type: 'file', discovered: false },
]
const files2 = [
{ path: '/c', type: 'folder', discovered: true },
{ path: '/c/.d', type: 'file', discovered: true },
]
const expected = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/.b', type: 'file', discovered: false },
{ path: '/c', type: 'folder', discovered: true },
{ path: '/c/.d', type: 'file', discovered: true },
]
mergeTest(files1, files2, expected)
})
test('remove-child', () => {
const root = {
path: '/',
type: 'folder',
}
const files = [
root,
{ path: '/a', type: 'file' },
{ path: '/b', type: 'folder' },
{ path: '/b/c', type: 'folder' },
{ path: '/b/d', type: 'file' },
{ path: '/b/c/f', type: 'file' },
]
const fileToRemove = { path: '/b/c', type: 'folder' }
const expected = [
root,
{ path: '/a', type: 'file' },
{ path: '/b', type: 'folder' },
{ path: '/b/d', type: 'file' },
]
removeChildTest(files, fileToRemove, expected)
})
})
test('load nested fileNode', () => {
const data = {
path: '/',
type: 'folder',
children: [
{
path: '/a',
type: 'file',
},
{
path: '/b',
type: 'folder',
children: [
{
path: '/b/c',
type: 'folder',
children: [
{
path: '/b/c/d',
type: 'file',
},
],
},
],
},
],
}
const node = new FileNode(data)
expect(node.children.length).toBe(2)
expect(node.children[1].children.length).toBe(1)
expect(node.children[1].children[0].children.length).toBe(1)
})
+3
View File
@@ -0,0 +1,3 @@
export { default as File } from './file.js'
export { default as FileNode } from './fileNode.js'
export { default as FileGraph } from './fileGraph.js'
+13
View File
@@ -0,0 +1,13 @@
export function dirname(path) {
const parts = path.split('/')
if (parts.length <= 2) {
return '/'
}
parts.pop()
return parts.join('/')
}
export function basename(path) {
const parts = path.split('/')
return parts.pop()
}