als-store
Version:
Library for streamlined file management and advanced data caching, featuring intelligent file searching, dynamic cache control, and flexible file operations
343 lines (310 loc) • 13.9 kB
JavaScript
const assert = require('node:assert')
const { describe, it, beforeEach, afterEach } = require('node:test')
const Store = require('../lib/store')
const { join } = require('node:path')
const File = require('../lib/file')
const { ensureDirSync, removeSync, existsSync, readFileSync } = require('fs-extra')
const { readdir, readdirSync } = require('als-readdir')
const delay = (ms, toResolve) => new Promise((resolve) => {
setTimeout(() => { resolve(toResolve) }, ms);
});
function generateRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const rootPath = join(__dirname, '..')
describe('basic tests', () => {
it('constructor', () => {
assert.throws(() => new Store({ dirPath: 'aa' }))
assert.throws(() => new Store({ dirPath: 'a'.repeat(256) }))
const dirPath = rootPath
assert.throws(() => new Store({ dirPath, maxAge: 'fgh' }))
assert.throws(() => new Store({ dirPath, maxActiveAge: 500 }))
// assert.throws(() => new Store({dirPath,schema:''}))
})
it('async get()', async () => {
const store = new Store({ dirPath: __dirname })
const { results, errors } = await store.get()
const files = readdirSync(__dirname).filter(t => t !== 'test')
assert(results.length === files.length)
assert(errors.length === 0)
const names = results.map(({ name }) => name)
assert(names.includes('store.test.js'))
assert.deepEqual(Object.keys(results[0].stats), File.stats, 'All files has stats')
})
it('async first()', async () => {
const store = new Store({ dirPath: __dirname })
const file = await store.first()
assert(file.name.endsWith('test.js'))
})
it('values()', async () => {
const store = new Store({ dirPath: __dirname })
const { results: r1 } = await store.get()
assert(r1[0].value === null)
const { results: r2 } = await store.values().get()
assert(typeof r2[0].value === 'string')
})
it('dir(dir)', async () => {
// TODO check param validation
const store = new Store({ dirPath: rootPath })
const { results: r1 } = await store.dir('node_modules/als-schema').get()
const names = r1.map(({ name }) => name)
assert(names.includes('package.json'))
assert.rejects(async () => await store.dir('kjdhfkjgh').get(), 'trows error if no such dir')
})
it('filter(fn)', async () => {
// TODO check param validation
const store = new Store({ dirPath: rootPath })
let { results: all } = await store.get()
all = [...all]
const mdFilter = file => file.name.endsWith('.md')
let { results: mdFiles } = await store.filter(mdFilter).get()
assert(all.length > mdFiles.length)
const index = generateRandomInteger(0, mdFiles.length - 1)
assert(mdFiles[index].name.endsWith('.md'))
mdFiles = [...mdFiles]
const smallFilter = file => file.stats.size < 1024
let { results: smallMdFiles } = await store.filter(mdFilter, smallFilter).get()
assert(smallMdFiles.length < mdFiles.length)
const index1 = generateRandomInteger(0, smallMdFiles.length - 1)
assert(mdFiles.includes(smallMdFiles[index1]))
smallMdFiles = [...smallMdFiles]
let { results: smallMdFiles1 } = await store.filter(mdFilter).filter(smallFilter).get()
assert(smallMdFiles.length === smallMdFiles1.length)
})
it('filter by params', async () => {
const schema = ['name', 'ext', 'ext1', 'ext2', 'ext3']
const store = new Store({ dirPath: rootPath, schema })
const testFilter = ({ params }) => params.ext === 'test' && params.ext1 === 'js'
const { results, errors } = await store.filter(testFilter).get()
const names = results.map(({ name }) => name)
assert(names[0].endsWith('test.js'))
const { results: pljson } = await store.filter(({ params }) => params.ext === 'package-lock' && params.ext1 === 'json').get()
assert(pljson.length === 1)
})
it('limit and skip', async () => {
const store = new Store({ dirPath: rootPath })
let { results: all } = await store.get()
all = [...all]
let { results: limited } = await store.limit(10).get()
assert(limited.length === 10)
let { results: skiped } = await store.skip(10).get()
assert(all.length - 10 === skiped.length)
assert(skiped[0] === all[10])
let { results: skipedAndLimited } = await store.skip(10).limit(10).get()
assert(skipedAndLimited.length === 10)
assert(skiped[0] === skipedAndLimited[0])
})
it('level', async () => {
const store = new Store({ dirPath: rootPath })
let { results: all } = await store.get()
all = [...all]
let { results: level3 } = await store.level(3).get()
assert(all.length > level3.length)
level3 = [...level3]
let { results: level2 } = await store.level(2).get()
assert(level3.length > level2.length)
let { results: level0 } = await store.level(0).get()
let files = readdirSync(rootPath,{withFileTypes:true}).filter(item => item.isFile())
files = files.filter(file => file.isFile())
assert(level0.length === files.length)
})
})
describe('sort and desc', () => {
let dirPath = rootPath
it('sort and desc', async () => {
const store = new Store({ dirPath })
let { results: all } = await store.get()
all = [...all]
let { results: size } = await store.sort('-stats.size').get()
assert(all[0].stats.size < size[0].stats.size)
let { results: sizeDesc } = await store.sort('stats.size').desc().get()
assert(all[0].stats.size < sizeDesc[0].stats.size)
sizeDesc = [...sizeDesc]
let { results: sizeDesc1 } = await store.sort('-stats.size').get()
assert(sizeDesc[0].stats.size === sizeDesc1[0].stats.size)
let { results: byName } = await store.sort('name').get()
assert(all[0].name !== byName[0].name)
assert(byName[0].name.match(/^\.|\_/) !== null)
})
it('sort by params', async () => {
const schema = ['name', 'ext', 'ext1', 'ext2', 'ext3']
const store = new Store({ dirPath, schema })
let { results: all } = await store.get()
all = [...all]
const { results: sortedExt } = await store.sort('params.ext').get()
assert(all[0].name !== sortedExt[0].name)
const { results: sortedExt1 } = await store.sort('params.ext1').get()
assert(sortedExt1[0].params.ext1 === undefined)
const { results: sortedExt1Desc } = await store.sort('-params.ext1').get()
assert(sortedExt1Desc[0].params.ext1 !== undefined)
})
it('sort with filter', async () => {
const store = new Store({ dirPath })
let { results: sorted } = await store.sort('name').get()
sorted = [...sorted]
const filter = ({ name }) => !name.startsWith('.') && !name.startsWith('_')
let { results: filteredAndSorted } = await store.filter(filter).sort('name').get()
assert(sorted[0].name !== filteredAndSorted[0].name) // sort should apply after filter
})
it('sort with skip and limit', async () => {
const store = new Store({ dirPath })
let { results: sorted } = await store.sort('name').get()
sorted = [...sorted]
let { results: skiped } = await store.skip(10).sort('name').get()
assert(sorted[10].name === skiped[0].name) // skip and limit should apply after sort
})
})
describe('create(name)', () => {
const store = new Store({ dirPath: __dirname })
it('validation', () => {
assert.throws(() => store.create(5))
assert.throws(() => store.create('a'.repeat(256)))
})
it('create simple file', () => {
const file = store.create('test')
assert(file instanceof File)
assert(file.cacheDir['test'] === file)
})
it('create file in directory', () => {
const dir = 'some/test'
const file = store.dir(dir).create('test')
assert(file.path === join(__dirname, dir))
})
})
describe('remove and rename dir,maxAge', () => {
const dirPath = join(__dirname, 'test')
const store = new Store({ dirPath })
beforeEach(() => ensureDirSync(dirPath))
afterEach(() => removeSync(dirPath))
async function createStructure(maxD = 5, maxI = 5) {
let path = []
for (let d = 0; d < maxD; d++) {
path.push('level' + d)
for (let i = 0; i < maxI; i++) {
const file = store.dir(path.join('/')).create(`test${d}_${i}.txt`)
file.value = `${d}_${i}`
await file.save()
}
}
}
function inCache(results) {
const index = generateRandomInteger(0, results.length - 1)
const file = results[index]
if (!File.map.has(file)) return false
const { path, name } = file
return File.getDir(path).cache[name] === file
}
it('remove files by filter', async () => {
for (let i = 0; i < 10; i++) {
const file = store.create(`test${i}.txt`)
await file.save()
}
const { results: all } = await store.get()
assert(all.length === 10)
const filter = ({ name }) => Number(name.split('.')[0].slice(-1)) > 5
await store.filter(filter).remove()
const { results: afterRemove } = await store.get()
assert(afterRemove.length === 6) // 6,7,8,9 removed
await store.remove()
const { results: shouldRemoveAll } = await store.get()
assert(shouldRemoveAll.length === 0)
})
it('remove directory', async () => {
const file = store.create(`test.txt`)
for (let d = 0; d < 3; d++) {
await file.save()
for (let i = 0; i < 5; i++) {
const file = store.dir(`dir${d}`).create(`test${i}.txt`)
await file.save()
}
}
const { results: all } = await store.get() // 16
const fileInDir = await store.dir('dir0').first()
assert(fileInDir.cacheDir[fileInDir.name] === fileInDir)
const allN = all.length
assert(inCache(all))
assert(existsSync(join(dirPath, 'dir0')))
await store.dir('dir0').remove()
assert(!existsSync(join(dirPath, 'dir0')))
const { results: dir0Removed } = await store.get() // 11
assert(dir0Removed.length === allN - 5)
assert(fileInDir.cacheDir[fileInDir.name] !== fileInDir)
})
it('remove nested directory and by level', async () => {
await createStructure()
const { results: all } = await store.get()
assert(all.length === 25)
await store.level(1).remove()
const { results: level1Removed } = await store.get()
assert(level1Removed.length === 20)
await store.level(3).remove()
const { results: level3Removed } = await store.get()
assert(level3Removed.length === 10)
})
it('rename dir', async () => {
await createStructure()
await store.get() // for adding files to cache
const oldName = 'level0', newName = 'renamed'
const oldPath = join(dirPath, oldName)
const newPath = join(dirPath, newName)
assert(existsSync(oldPath))
assert(File.getDir(oldPath).cache !== undefined)
assert(File.getDir(newPath).cache === undefined)
await store.renameDir(oldName, newName)
assert(existsSync(newPath))
assert(File.getDir(oldPath).cache === undefined)
assert(File.getDir(newPath).cache !== undefined)
assert(File.getDir(newPath).cache['test0_1.txt'].path.endsWith(newName))
})
it('rename nested', async () => {
await createStructure()
const dir = 'level0/level1/level2'
const oldPath = join(dirPath, dir, 'level3')
const newPath = join(dirPath, dir, 'renamed')
assert(existsSync(oldPath))
assert(!existsSync(newPath))
assert(File.getDir(oldPath).cache !== undefined)
assert(File.getDir(newPath).cache === undefined)
await store.dir(dir).renameDir('level3', 'renamed')
assert(!existsSync(oldPath))
assert(existsSync(newPath))
assert(File.getDir(oldPath).cache === undefined)
assert(File.getDir(newPath).cache !== undefined)
assert(File.getDir(newPath).cache['test3_4.txt'].path.endsWith('renamed'))
})
it('validate maxAge and maxActiveAge', () => {
assert.throws(() => new Store({ dirPath, maxAge: -1 }))
assert.throws(() => new Store({ dirPath, maxAge: '' }))
})
it('maxAge', async () => {
await createStructure(10,10)
store.maxAge = 500
const { results: all } = await store.get()
assert(all.length === 100)
const file = all[generateRandomInteger(0,49)]
const file1 = all[generateRandomInteger(50,99)]
await delay(500)
const { results: maxAged } = await store.get()
assert(maxAged.length === 0)
assert(store.toRemove.length === 50)
await delay(200) // give time to delete outdated
assert(existsSync(join(file.path,file.name)) === false)
assert(existsSync(join(file1.path,file1.name)))
await store.get()
await delay(200) // give time to delete outdated
assert(existsSync(join(file1.path,file1.name)) === false)
store.maxAge = undefined
})
it('maxActiveAge', async () => {
await createStructure(1,10)
store.maxActiveAge = 500
const { results: all } = await store.get()
assert(all.length === 10)
const file = all[generateRandomInteger(0,9)]
await file.getValue()
await file.save()
await delay(300)
const { results: maxAged } = await store.get()
assert(maxAged.length === 1)
})
})