├── .nvmrc ├── lib ├── types │ ├── index.ts │ └── entry.type.ts ├── utils │ ├── index.ts │ ├── jest │ │ ├── index.ts │ │ ├── to-match-snapshot │ │ │ ├── __snapshots__ │ │ │ │ └── to-match-snapshot.spec.ts.snap │ │ │ ├── to-match-snapshot.spec.ts │ │ │ └── to-match-snapshot.function.ts │ │ └── function-import-test │ │ │ ├── function-import-test.spec.ts │ │ │ └── function-import-test.function.ts │ └── functions │ │ ├── is-string │ │ ├── is-string.function.ts │ │ └── is-string.spec.ts │ │ ├── is-function │ │ ├── is-function.function.ts │ │ └── is-function.spec.ts │ │ ├── starts-with-slash │ │ ├── starts-with-slash.function.ts │ │ └── starts-with-slash.spec.ts │ │ ├── get-directory-name │ │ ├── get-directory-name.function.ts │ │ └── get-directory-name.spec.ts │ │ ├── is-valid-path │ │ ├── is-valid-path.function.ts │ │ └── is-valid-path.spec.ts │ │ ├── try-catch-wrapper │ │ ├── try-catch-wrapper.function.ts │ │ └── try-catch-wrapper.spec.ts │ │ ├── has-root-directory-prefix │ │ ├── has-root-directory-prefix.function.ts │ │ └── has-root-directory-prefix.spec.ts │ │ ├── with-root-directory-prefix │ │ ├── with-root-directory-prefix.function.ts │ │ └── with-root-directory-prefix.spec.ts │ │ ├── validate-create-fs-props │ │ ├── validate-create-fs-props.function.ts │ │ ├── validate-create-fs-props.schema.ts │ │ ├── validate-create-fs-props.spec.ts │ │ └── __snapshots__ │ │ │ └── validate-create-fs-props.spec.ts.snap │ │ ├── index.ts │ │ └── format-and-validate-full-path │ │ ├── format-and-validate-full-path.function.ts │ │ └── format-and-validate-full-path.spec.ts ├── database │ ├── is-indexeddb-support │ │ ├── is-indexeddb-support.function.ts │ │ └── is-indexeddb-support.spec.ts │ ├── get-record-instance │ │ ├── get-record-instance.types.ts │ │ ├── get-record-instance.function.ts │ │ └── get-record-instance.spec.ts │ ├── put-record-instance │ │ ├── put-record-instance.types.ts │ │ ├── put-record-instance.function.ts │ │ └── put-record-instance.spec.ts │ ├── open-cursor-instance │ │ ├── open-cursor-instance.types.ts │ │ ├── open-cursor-instance.spec.ts │ │ └── open-cursor-instance.function.ts │ ├── delete-record-instance │ │ ├── delete-record-instance.types.ts │ │ ├── delete-record-instance.function.ts │ │ └── delete-record-instance.spec.ts │ ├── initialize-database │ │ ├── initialize-database.types.ts │ │ ├── initialize-database.spec.ts │ │ └── initialize-database.function.ts │ ├── initialize-object-store-instance │ │ ├── initialize-object-store-instance.types.ts │ │ ├── initialize-object-store-instance.function.ts │ │ └── initialize-object-store-instance.spec.ts │ ├── open-indexeddb-connection │ │ ├── open-indexeddb-connection.function.ts │ │ └── open-indexeddb-connection.spec.ts │ ├── get-database-crud │ │ ├── get-database-crud.spec.ts │ │ ├── get-database-crud.types.ts │ │ └── get-database-crud.function.ts │ └── index.ts ├── constants.ts ├── framework │ ├── parts │ │ ├── read-file-instance │ │ │ ├── read-file-instance.types.ts │ │ │ ├── read-file-instance.function.ts │ │ │ └── read-file-instance.spec.ts │ │ ├── exists-instance │ │ │ ├── exists-instance.types.ts │ │ │ ├── exists-instance.function.ts │ │ │ └── exists-instance.spec.ts │ │ ├── remove-instance │ │ │ ├── remove-instance.types.ts │ │ │ ├── remove-instance.function.ts │ │ │ └── remove-instance.spec.ts │ │ ├── create-root-directory-instance │ │ │ ├── create-root-directory-instance.types.ts │ │ │ └── create-root-directory-instance.function.ts │ │ ├── remove-file-instance │ │ │ ├── remove-file-instance.types.ts │ │ │ ├── remove-file-instance.function.ts │ │ │ └── remove-file-instance.spec.ts │ │ ├── write-file-instance │ │ │ ├── write-file-instance.types.ts │ │ │ ├── write-file-instance.function.ts │ │ │ └── write-file-instance.spec.ts │ │ ├── is-file-instance │ │ │ ├── is-file-instance.types.ts │ │ │ ├── is-file-instance.function.ts │ │ │ └── is-file-instance.spec.ts │ │ ├── file-details-instance │ │ │ ├── file-details-instance.types.ts │ │ │ ├── file-details-instance.function.ts │ │ │ └── file-details-instance.spec.ts │ │ ├── is-directory-instance │ │ │ ├── is-directory-instance.types.ts │ │ │ ├── is-directory-instance.function.ts │ │ │ └── is-directory-instance.spec.ts │ │ ├── directory-details-instance │ │ │ ├── directory-details-instance.types.ts │ │ │ ├── directory-details-instance.function.ts │ │ │ └── directory-details-instance.spec.ts │ │ ├── create-directory-instance │ │ │ ├── create-directory-instance.types.ts │ │ │ ├── create-directory-instance.function.ts │ │ │ └── create-directory-instance.spec.ts │ │ ├── remove-directory-instance │ │ │ ├── remove-directory-instance.types.ts │ │ │ ├── remove-directory-instance.function.ts │ │ │ └── remove-directory-instance.spec.ts │ │ ├── update-file-details-instance │ │ │ ├── update-file-details-instance.types.ts │ │ │ ├── update-file-details-instance.function.ts │ │ │ └── update-file-details-instance.spec.ts │ │ ├── details-instance │ │ │ ├── details-instance.types.ts │ │ │ ├── details-instance.function.ts │ │ │ └── details-instance.spec.ts │ │ ├── rename-file-instance │ │ │ ├── rename-file-instance.types.ts │ │ │ ├── rename-file-instance.function.ts │ │ │ └── rename-file-instance.spec.ts │ │ ├── copy-file-instance │ │ │ ├── copy-file-instance.types.ts │ │ │ ├── copy-file-instance.function.ts │ │ │ └── copy-file-instance.spec.ts │ │ ├── move-file-instance │ │ │ ├── move-file-instance.types.ts │ │ │ ├── move-file-instance.function.ts │ │ │ └── move-file-instance.spec.ts │ │ ├── read-directory-instance │ │ │ ├── read-directory-instance.types.ts │ │ │ ├── read-directory-instance.function.ts │ │ │ └── read-directory-instance.spec.ts │ │ └── index.ts │ ├── create-fs.defaults.ts │ ├── create-fs.types.ts │ ├── __snapshots__ │ │ └── create-fs.spec.ts.snap │ ├── create-fs.function.ts │ └── create-fs.spec.ts └── index.ts ├── .prettierignore ├── scripts ├── reinstall-node-modules.sh ├── remove-local-branches.sh ├── git-checkout-develop-with-pull.sh ├── git-create-branch.sh ├── git-reset-local-changes.sh └── git-push-changes.sh ├── commitlint.config.js ├── .husky ├── pre-commit └── commit-msg ├── tsconfig.build.json ├── .eslintignore ├── .gitattributes ├── .editorconfig ├── .yarnrc.yml ├── .lintstagedrc.js ├── jest.config.js ├── tsconfig.paths.json ├── .prettierrc ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── rollup.config.js ├── .gitignore ├── .eslintrc.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.15.0 2 | -------------------------------------------------------------------------------- /lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entry.type'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.ts.snap 3 | 4 | dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /scripts/reinstall-node-modules.sh: -------------------------------------------------------------------------------- 1 | rm -rf node_modules 2 | yarn install 3 | -------------------------------------------------------------------------------- /lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jest'; 2 | export * from './functions'; 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /scripts/remove-local-branches.sh: -------------------------------------------------------------------------------- 1 | yarn grlc 2 | git branch | grep -v "develop" | xargs git branch -D 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /lib/database/is-indexeddb-support/is-indexeddb-support.function.ts: -------------------------------------------------------------------------------- 1 | export const isIndexedDBSupport = (): boolean => Boolean(indexedDB); 2 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["dist", "jest/**/*.ts", "node_modules", "/**/*.test.ts", "/**/*.spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.ts.snap 3 | 4 | dist 5 | node_modules 6 | 7 | .eslintrc.js 8 | jest.config.js 9 | rollup.config.js 10 | commitlint.config.js 11 | -------------------------------------------------------------------------------- /lib/utils/jest/index.ts: -------------------------------------------------------------------------------- 1 | export * from './to-match-snapshot/to-match-snapshot.function'; 2 | export * from './function-import-test/function-import-test.function'; 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /lib/utils/functions/is-string/is-string.function.ts: -------------------------------------------------------------------------------- 1 | export const isString = (value: unknown): value is string => typeof value === 'string' || value instanceof String; 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /lib/database/get-record-instance/get-record-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IGetRecordInstanceProps { 2 | initializeObjectStore: (type: IDBTransactionMode) => Promise; 3 | } 4 | -------------------------------------------------------------------------------- /lib/database/put-record-instance/put-record-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IPutRecordInstanceProps { 2 | initializeObjectStore: (type: IDBTransactionMode) => Promise; 3 | } 4 | -------------------------------------------------------------------------------- /lib/database/open-cursor-instance/open-cursor-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IOpenCursorInstanceProps { 2 | initializeObjectStore: (type: IDBTransactionMode) => Promise; 3 | } 4 | -------------------------------------------------------------------------------- /lib/database/delete-record-instance/delete-record-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IDeleteRecordInstanceProps { 2 | initializeObjectStore: (type: IDBTransactionMode) => Promise; 3 | } 4 | -------------------------------------------------------------------------------- /lib/database/initialize-database/initialize-database.types.ts: -------------------------------------------------------------------------------- 1 | export interface IInitializeDatabaseProps { 2 | databaseName: string; 3 | databaseVersion: number; 4 | objectStoreName: string; 5 | } 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.5.0.cjs 8 | -------------------------------------------------------------------------------- /lib/utils/jest/to-match-snapshot/__snapshots__/to-match-snapshot.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`toMatchSnapshot Function snapshot test should match snapshot 1`] = `undefined`; 4 | -------------------------------------------------------------------------------- /lib/database/initialize-object-store-instance/initialize-object-store-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IInitializeObjectStoreInstanceProps { 2 | databaseName: string; 3 | databaseVersion: number; 4 | objectStoreName: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/database/open-indexeddb-connection/open-indexeddb-connection.function.ts: -------------------------------------------------------------------------------- 1 | export const openIndexedDBConnection = (databaseName: string, databaseVersion?: number): IDBOpenDBRequest => 2 | indexedDB.open(databaseName, databaseVersion); 3 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const OBJECT_STORE_KEY_PATH = 'fullPath'; 2 | export const OBJECT_STORE_INDEX_NAME = 'directory'; 3 | 4 | export const IS_VALID_PATH_REG_EXP_STRING = '^([A-Za-z]:|[A-Za-z0-9_-]+(.[A-Za-z0-9_-]+)*)((/[A-Za-z0-9_.-]+)+)$'; 5 | -------------------------------------------------------------------------------- /lib/framework/parts/read-file-instance/read-file-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IFileEntry } from '@types'; 2 | 3 | export interface IReadFileInstanceProps { 4 | fileDetails: (fullPath: string) => Promise>; 5 | } 6 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.lib/**/*': ['prettier --write', 'eslint --fix'], 3 | '*.{js,css,md}': 'prettier --write', 4 | '*.ts': [() => 'tsc --skipLibCheck --noEmit'], 5 | 'package.json': ['prettier-package-json --write'], 6 | }; 7 | -------------------------------------------------------------------------------- /lib/framework/parts/exists-instance/exists-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IExistsInstanceProps { 2 | getRecord: (query: IDBValidKey | IDBKeyRange, onResolve: (target: IDBRequest) => TValue) => Promise; 3 | rootDirectoryName: string; 4 | } 5 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-instance/remove-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IRemoveInstanceProps { 2 | deleteRecord: (key: IDBValidKey | IDBKeyRange) => Promise; 3 | exists: (fullPath: string) => Promise; 4 | rootDirectoryName: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/framework/parts/create-root-directory-instance/create-root-directory-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface ICreateRootDirectoryInstanceProps { 2 | putRecord: (value: TValue, key?: IDBValidKey) => Promise; 3 | rootDirectoryName: string; 4 | } 5 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-file-instance/remove-file-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IRemoveFileInstanceProps { 2 | deleteRecord: (key: IDBValidKey | IDBKeyRange) => Promise; 3 | 4 | isFile: (fullPath: string) => Promise; 5 | rootDirectoryName: string; 6 | } 7 | -------------------------------------------------------------------------------- /lib/framework/create-fs.defaults.ts: -------------------------------------------------------------------------------- 1 | import { ICreateFsProps } from './create-fs.types'; 2 | 3 | export const defaultProps: Required = { 4 | databaseVersion: 1, 5 | objectStoreName: 'files', 6 | rootDirectoryName: 'root', 7 | databaseName: 'indexeddb-fs', 8 | }; 9 | -------------------------------------------------------------------------------- /lib/framework/parts/write-file-instance/write-file-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IWriteFileInstanceProps { 2 | isDirectory: (fullPath: string) => Promise; 3 | putRecord: (value: TValue, key?: IDBValidKey) => Promise; 4 | rootDirectoryName: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/framework/parts/is-file-instance/is-file-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IIsFileInstanceProps { 2 | exists: (fullPath: string) => Promise; 3 | getRecord: (query: IDBValidKey | IDBKeyRange, onResolve: (target: IDBRequest) => TValue) => Promise; 4 | rootDirectoryName: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/utils/functions/is-function/is-function.function.ts: -------------------------------------------------------------------------------- 1 | export const isFunction = (functionToCheck: unknown): functionToCheck is Function => 2 | !!( 3 | typeof functionToCheck === 'function' && 4 | !!functionToCheck.constructor && 5 | !!functionToCheck.call && 6 | !!functionToCheck.apply 7 | ); 8 | -------------------------------------------------------------------------------- /lib/utils/functions/starts-with-slash/starts-with-slash.function.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@utils'; 2 | 3 | export function startsWithSlash(fullPath: string): boolean { 4 | if (!isString(fullPath) || fullPath.length === 0) { 5 | return false; 6 | } 7 | 8 | return fullPath[0] === '/'; 9 | } 10 | -------------------------------------------------------------------------------- /lib/framework/parts/file-details-instance/file-details-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IFileDetailsInstanceProps { 2 | getRecord: (query: IDBValidKey | IDBKeyRange, onResolve: (target: IDBRequest) => TValue) => Promise; 3 | isFile: (fullPath: string) => Promise; 4 | rootDirectoryName: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/framework/parts/is-directory-instance/is-directory-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IIsDirectoryInstanceProps { 2 | exists: (fullPath: string) => Promise; 3 | getRecord: (query: IDBValidKey | IDBKeyRange, onResolve: (target: IDBRequest) => TValue) => Promise; 4 | rootDirectoryName: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/framework/parts/directory-details-instance/directory-details-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface IDirectoryDetailsInstanceProps { 2 | getRecord: (query: IDBValidKey | IDBKeyRange, onResolve: (target: IDBRequest) => TValue) => Promise; 3 | isDirectory: (fullPath: string) => Promise; 4 | rootDirectoryName: string; 5 | } 6 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | const fs = createFs(); 4 | 5 | export default fs; 6 | 7 | export { IFileEntry, IDirectoryEntry } from '@types'; 8 | 9 | export { ICreateFsProps, ICreateFsOutput } from '@framework/create-fs.types'; 10 | 11 | export { createFs } from '@framework/create-fs.function'; 12 | -------------------------------------------------------------------------------- /lib/framework/parts/create-directory-instance/create-directory-instance.types.ts: -------------------------------------------------------------------------------- 1 | export interface ICreateDirectoryInstanceProps { 2 | isDirectory: (fullPath: string) => Promise; 3 | isFile: (fullPath: string) => Promise; 4 | putRecord: (value: TValue, key?: IDBValidKey) => Promise; 5 | rootDirectoryName: string; 6 | } 7 | -------------------------------------------------------------------------------- /lib/utils/functions/get-directory-name/get-directory-name.function.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export function getDirectoryName(fullPath: string, rootDirectoryName: string): string { 4 | const directoryName = path.dirname(fullPath); 5 | 6 | if (directoryName === '.') { 7 | return rootDirectoryName; 8 | } 9 | 10 | return directoryName; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/git-checkout-develop-with-pull.sh: -------------------------------------------------------------------------------- 1 | developBranchName=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p') 2 | echo "Info: Detach develop branch name as: '$developBranchName'." 3 | 4 | git checkout $developBranchName 5 | echo "Info: Checkout to '$developBranchName' branch." 6 | 7 | git pull origin $developBranchName 8 | echo "Info: Pull changes from '$developBranchName' branch." 9 | -------------------------------------------------------------------------------- /lib/framework/parts/read-file-instance/read-file-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { IReadFileInstanceProps } from './read-file-instance.types'; 2 | 3 | export const readFileInstance = 4 | ({ fileDetails }: IReadFileInstanceProps) => 5 | async (fullPath: string): Promise => { 6 | const result = await fileDetails(fullPath); 7 | 8 | return result.data as TData; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/utils/functions/is-valid-path/is-valid-path.function.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@utils'; 2 | 3 | import { IS_VALID_PATH_REG_EXP_STRING } from '@constants'; 4 | 5 | const pathRegExp = new RegExp(IS_VALID_PATH_REG_EXP_STRING); 6 | 7 | export function isValidPath(path: string): boolean { 8 | if (!isString(path)) { 9 | return false; 10 | } 11 | 12 | return pathRegExp.test(path); 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/jest/to-match-snapshot/to-match-snapshot.spec.ts: -------------------------------------------------------------------------------- 1 | import { toMatchSnapshot } from '@utils'; 2 | 3 | describe('toMatchSnapshot Function', () => { 4 | toMatchSnapshot(jest.fn()); 5 | 6 | it('should throw an error when passed parameter is not a function', () => { 7 | // @ts-expect-error 8 | expect(() => toMatchSnapshot(null)).toThrow('parameter is not a function'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/database/is-indexeddb-support/is-indexeddb-support.spec.ts: -------------------------------------------------------------------------------- 1 | import { isIndexedDBSupport } from '@database'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | describe('isIndexedDBSupport Function', () => { 6 | functionImportTest(isIndexedDBSupport); 7 | 8 | it('should return true if browser supports indexedDB', () => { 9 | expect(isIndexedDBSupport()).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-directory-instance/remove-directory-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IReadDirectoryInstanceOutput } from '..'; 2 | 3 | export interface IRemoveDirectoryInstanceProps { 4 | isDirectory: (fullPath: string) => Promise; 5 | readDirectory: (fullPath: string) => Promise; 6 | remove: (fullPath: string) => Promise; 7 | rootDirectoryName: string; 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/functions/try-catch-wrapper/try-catch-wrapper.function.ts: -------------------------------------------------------------------------------- 1 | export const tryCatchWrapper = async ( 2 | func: () => Promise, 3 | onError?: (error: unknown) => void, 4 | ): Promise => { 5 | try { 6 | return await func(); 7 | } catch (error: unknown) { 8 | if (onError) { 9 | onError(error); 10 | } 11 | } 12 | 13 | return null; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/utils/jest/function-import-test/function-import-test.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest } from '@utils'; 2 | 3 | describe('functionImportTest Function', () => { 4 | functionImportTest(jest.fn()); 5 | 6 | it('should throw an error when passed parameter is not a function', () => { 7 | // @ts-expect-error 8 | expect(() => functionImportTest(null)).toThrow('parameter is not a function'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/utils/jest/to-match-snapshot/to-match-snapshot.function.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@utils'; 2 | 3 | export function toMatchSnapshot(func: Function): void { 4 | if (!isFunction(func)) { 5 | throw new Error('parameter is not a function'); 6 | } 7 | 8 | describe('snapshot test', () => { 9 | it('should match snapshot', () => { 10 | expect(func()).toMatchSnapshot(); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const mapPathsFromTsConfig = require('jest-module-name-mapper').default; 2 | 3 | module.exports = { 4 | testEnvironment: 'node', 5 | roots: ['/lib'], 6 | setupFiles: ['fake-indexeddb/auto'], 7 | transform: { '^.+\\.ts$': 'ts-jest' }, 8 | moduleNameMapper: mapPathsFromTsConfig(), 9 | modulePathIgnorePatterns: ['node_modules'], 10 | testMatch: ['**/?(*.)+(spec|test).+(ts|js)'], 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/jest/function-import-test/function-import-test.function.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@utils'; 2 | 3 | export function functionImportTest(func: Function): void { 4 | if (!isFunction(func)) { 5 | throw new Error('parameter is not a function'); 6 | } 7 | 8 | describe('import test', () => { 9 | it(`should import ${func.name}`, () => { 10 | expect(typeof func).toBe('function'); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/framework/parts/update-file-details-instance/update-file-details-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IFileEntry } from '@types'; 2 | 3 | export interface IUpdateFileDetailsInstanceProps { 4 | fileDetails: (fullPath: string) => Promise>; 5 | isDirectory: (fullPath: string) => Promise; 6 | putRecord: (value: TValue, key?: IDBValidKey) => Promise; 7 | rootDirectoryName: string; 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/functions/has-root-directory-prefix/has-root-directory-prefix.function.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@utils'; 2 | 3 | export function hasRootDirectoryPrefix(fullPath: string, rootDirectoryName: string): boolean { 4 | if (!isString(fullPath) || !isString(rootDirectoryName) || rootDirectoryName === '') { 5 | return false; 6 | } 7 | 8 | const rootDirectoryNameWithSlash = `${rootDirectoryName}/`; 9 | 10 | return fullPath.startsWith(rootDirectoryNameWithSlash); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/git-create-branch.sh: -------------------------------------------------------------------------------- 1 | branchName="$1" 2 | 3 | if [ -z "$branchName" ]; then 4 | echo 'Error: branch name was not specified.' 5 | 6 | exit 1 7 | fi 8 | 9 | exists=$(git show-ref refs/heads/$branchName) 10 | if [ -n "$exists" ]; then 11 | echo "Error: '$branchName' branch already exists." 12 | 13 | exit 1 14 | fi 15 | 16 | git branch $branchName 17 | echo "Info: Created branch with name: '$branchName'." 18 | 19 | git checkout $branchName 20 | echo "Info: Checkout to created branch." 21 | -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@types/*": ["./lib/types/*"], 6 | "@types": ["./lib/types"], 7 | "@utils/*": ["./lib/utils/*"], 8 | "@utils": ["./lib/utils"], 9 | "@constants": ["./lib/constants"], 10 | "@framework/*": ["./lib/framework/*"], 11 | "@framework": ["./lib/framework"], 12 | "@database/*": ["./lib/database/*"], 13 | "@database": ["./lib/database"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/types/entry.type.ts: -------------------------------------------------------------------------------- 1 | export enum EEntryType { 2 | DIRECTORY = 'directory', 3 | FILE = 'file', 4 | } 5 | 6 | interface IEntry { 7 | createdAt: number; 8 | directory: string; 9 | fullPath: string; 10 | name: string; 11 | type: TType; 12 | } 13 | 14 | export interface IFileEntry extends IEntry { 15 | data: TData; 16 | } 17 | 18 | export interface IDirectoryEntry extends IEntry { 19 | isRoot: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /lib/framework/parts/details-instance/details-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IDirectoryEntry, IFileEntry } from '@types'; 2 | 3 | export interface IDetailsInstanceProps { 4 | directoryDetails: (fullPath: string) => Promise; 5 | exists: (fullPath: string) => Promise; 6 | fileDetails: (fullPath: string) => Promise>; 7 | isDirectory: (fullPath: string) => Promise; 8 | isFile: (fullPath: string) => Promise; 9 | rootDirectoryName: string; 10 | } 11 | -------------------------------------------------------------------------------- /lib/framework/parts/rename-file-instance/rename-file-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IFileEntry } from '@types'; 2 | 3 | export interface IRenameFileInstanceProps { 4 | exists: (fullPath: string) => Promise; 5 | isFile: (fullPath: string) => Promise; 6 | removeFile: (fullPath: string) => Promise; 7 | rootDirectoryName: string; 8 | updateFileDetails: ( 9 | fullPath: string, 10 | newFileEntry: Partial>, 11 | ) => Promise>; 12 | } 13 | -------------------------------------------------------------------------------- /lib/framework/parts/copy-file-instance/copy-file-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IFileEntry } from '@types'; 2 | 3 | export interface ICopyFileInstanceProps { 4 | exists: (fullPath: string) => Promise; 5 | fileDetails: (fullPath: string) => Promise>; 6 | isDirectory: (fullPath: string) => Promise; 7 | isFile: (fullPath: string) => Promise; 8 | rootDirectoryName: string; 9 | writeFile: (fullPath: string, data: TData) => Promise>; 10 | } 11 | -------------------------------------------------------------------------------- /lib/framework/parts/move-file-instance/move-file-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IFileEntry } from '@types'; 2 | 3 | export interface IMoveFileInstanceProps { 4 | exists: (fullPath: string) => Promise; 5 | isDirectory: (fullPath: string) => Promise; 6 | isFile: (fullPath: string) => Promise; 7 | removeFile: (fullPath: string) => Promise; 8 | rootDirectoryName: string; 9 | updateFileDetails: ( 10 | fullPath: string, 11 | newFileEntry: Partial>, 12 | ) => Promise>; 13 | } 14 | -------------------------------------------------------------------------------- /lib/framework/parts/exists-instance/exists-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { IExistsInstanceProps } from './exists-instance.types'; 4 | 5 | const onResolve = ({ result }: IDBRequest) => Boolean(result?.createdAt); 6 | 7 | export const existsInstance = 8 | ({ getRecord, rootDirectoryName }: IExistsInstanceProps) => 9 | async (fullPath: string): Promise => { 10 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 11 | 12 | return getRecord(verifiedFullPath, onResolve); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/utils/functions/get-directory-name/get-directory-name.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, getDirectoryName } from '@utils'; 2 | 3 | describe('getDirectoryName Function', () => { 4 | functionImportTest(getDirectoryName); 5 | 6 | it('should return passed root directory name when value is not in any specified directory', () => { 7 | expect(getDirectoryName('example', 'root')).toEqual('root'); 8 | }); 9 | 10 | it('should return directory where value is existing', () => { 11 | expect(getDirectoryName('example/directory', 'root')).toEqual('example'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/database/put-record-instance/put-record-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { IPutRecordInstanceProps } from './put-record-instance.types'; 2 | 3 | export const putRecordInstance = 4 | ({ initializeObjectStore }: IPutRecordInstanceProps) => 5 | async (value: TValue): Promise => { 6 | const objectStore = await initializeObjectStore('readwrite'); 7 | 8 | return new Promise((resolve, reject) => { 9 | const request = objectStore.put(value); 10 | 11 | request.onerror = reject; 12 | request.onsuccess = () => resolve(value); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/database/delete-record-instance/delete-record-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { IDeleteRecordInstanceProps } from './delete-record-instance.types'; 2 | 3 | export const deleteRecordInstance = 4 | ({ initializeObjectStore }: IDeleteRecordInstanceProps) => 5 | async (key: IDBValidKey | IDBKeyRange): Promise => { 6 | const objectStore = await initializeObjectStore('readwrite'); 7 | 8 | return new Promise((resolve, reject) => { 9 | const request = objectStore.delete(key); 10 | 11 | request.onerror = reject; 12 | 13 | request.onsuccess = () => resolve(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "importOrder": [ 8 | "", 9 | "^@framework", 10 | "^@framework/(.*)$", 11 | "^@database", 12 | "^@database/(.*)$", 13 | "^@utils", 14 | "^@utils/(.*)$", 15 | "^@constants", 16 | "^@types", 17 | "^@types/(.*)$", 18 | "^[./]" 19 | ], 20 | "importOrderSeparation": true, 21 | "importOrderSortSpecifiers": true, 22 | "importOrderGroupNamespaceSpecifiers": true, 23 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 24 | } 25 | -------------------------------------------------------------------------------- /lib/utils/functions/with-root-directory-prefix/with-root-directory-prefix.function.ts: -------------------------------------------------------------------------------- 1 | import { hasRootDirectoryPrefix, startsWithSlash } from '@utils'; 2 | 3 | export function withRootDirectoryPrefix(fullPath: string, rootDirectoryName: string): string | null { 4 | if (fullPath === '') { 5 | return rootDirectoryName; 6 | } 7 | 8 | const isRootPrefix = hasRootDirectoryPrefix(fullPath, rootDirectoryName); 9 | 10 | if (!isRootPrefix) { 11 | const withFirstSlash = startsWithSlash(fullPath); 12 | 13 | return `${rootDirectoryName}${withFirstSlash ? '' : '/'}${fullPath}`; 14 | } 15 | 16 | return fullPath; 17 | } 18 | -------------------------------------------------------------------------------- /scripts/git-reset-local-changes.sh: -------------------------------------------------------------------------------- 1 | while true; do 2 | read -p "Reset all of your local changes: commits, untracked files, untracked directories. Proceed? (Y/n)" yn 3 | case $yn in 4 | [Yy]*) break ;; 5 | [Nn]*) exit ;; 6 | *) break ;; 7 | esac 8 | done 9 | 10 | currentBranch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') 11 | 12 | # To remove untracked files 13 | git clean -f 14 | 15 | # To remove untracked directories 16 | git clean -fd 17 | 18 | # revert changes to index 19 | git reset --hard 20 | 21 | # remove all local commits 22 | git fetch origin 23 | git reset --hard origin/$currentBranch 24 | 25 | git fetch --all 26 | -------------------------------------------------------------------------------- /lib/utils/functions/validate-create-fs-props/validate-create-fs-props.function.ts: -------------------------------------------------------------------------------- 1 | import { validate } from 'jsonschema'; 2 | 3 | import { ICreateFsProps } from '@framework/create-fs.types'; 4 | 5 | import { createFsPropsSchema } from './validate-create-fs-props.schema'; 6 | 7 | export function validateCreateFsProps(props: Required): boolean { 8 | const { errors, valid } = validate(props, createFsPropsSchema); 9 | 10 | if (valid) { 11 | return valid; 12 | } 13 | 14 | const errorsAsString = JSON.stringify(errors); 15 | 16 | throw new Error(`Props passed to createFS function are invalid:\n${errorsAsString}`); 17 | } 18 | -------------------------------------------------------------------------------- /lib/database/get-database-crud/get-database-crud.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDatabaseCrud } from '@database'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | describe('getDatabaseCrud Function', () => { 6 | functionImportTest(getDatabaseCrud); 7 | 8 | it('should return proper functions', () => { 9 | const databaseCrud = getDatabaseCrud({ 10 | databaseVersion: 1, 11 | databaseName: 'getRecord', 12 | objectStoreName: 'objectStoreName', 13 | }); 14 | 15 | expect(databaseCrud).toBeDefined(); 16 | 17 | expect(Object.keys(databaseCrud)).toEqual(['getRecord', 'putRecord', 'openCursor', 'deleteRecord']); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/framework/parts/read-directory-instance/read-directory-instance.types.ts: -------------------------------------------------------------------------------- 1 | import { IDirectoryEntry, IFileEntry } from '@types'; 2 | 3 | export interface IReadDirectoryInstanceProps { 4 | isDirectory: (fullPath: string) => Promise; 5 | openCursor: ( 6 | value: unknown, 7 | onResolve: (target: IDBRequest, resolve: (value: TValue) => void) => TValue, 8 | ) => Promise; 9 | rootDirectoryName: string; 10 | } 11 | 12 | export interface IReadDirectoryInstanceOutput { 13 | directories: IDirectoryEntry[]; 14 | directoriesCount: number; 15 | files: Omit[]; 16 | filesCount: number; 17 | isEmpty: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /lib/database/get-record-instance/get-record-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { IGetRecordInstanceProps } from './get-record-instance.types'; 2 | 3 | export const getRecordInstance = 4 | ({ initializeObjectStore }: IGetRecordInstanceProps) => 5 | async (query: IDBValidKey | IDBKeyRange, onResolve: (target: IDBRequest) => TValue): Promise => { 6 | const objectStore = await initializeObjectStore('readonly'); 7 | 8 | return new Promise((resolve, reject) => { 9 | const request = objectStore.get(query); 10 | 11 | request.onerror = reject; 12 | 13 | request.onsuccess = (event: Event) => resolve(onResolve(event?.target as IDBRequest)); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/database/get-database-crud/get-database-crud.types.ts: -------------------------------------------------------------------------------- 1 | export interface IGetDatabaseCrudProps { 2 | databaseName: string; 3 | databaseVersion: number; 4 | objectStoreName: string; 5 | } 6 | 7 | export interface IGetDatabaseCrudOutput { 8 | deleteRecord: (key: IDBValidKey | IDBKeyRange) => Promise; 9 | getRecord: (query: IDBValidKey | IDBKeyRange, onResolve: (target: IDBRequest) => TValue) => Promise; 10 | openCursor: ( 11 | value: unknown, 12 | onResolve: (target: IDBRequest, resolve: (value: TValue) => void) => TValue, 13 | ) => Promise; 14 | putRecord: (value: TValue, key?: IDBValidKey) => Promise; 15 | } 16 | -------------------------------------------------------------------------------- /lib/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-database-crud/get-database-crud.function'; 2 | export * from './initialize-database/initialize-database.function'; 3 | export * from './put-record-instance/put-record-instance.function'; 4 | export * from './get-record-instance/get-record-instance.function'; 5 | export * from './open-cursor-instance/open-cursor-instance.function'; 6 | export * from './is-indexeddb-support/is-indexeddb-support.function'; 7 | export * from './delete-record-instance/delete-record-instance.function'; 8 | export * from './open-indexeddb-connection/open-indexeddb-connection.function'; 9 | export * from './initialize-object-store-instance/initialize-object-store-instance.function'; 10 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-instance/remove-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { IRemoveInstanceProps } from './remove-instance.types'; 4 | 5 | export const removeInstance = 6 | ({ deleteRecord, exists, rootDirectoryName }: IRemoveInstanceProps) => 7 | async (fullPath: string): Promise => { 8 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 9 | 10 | const doesTargetExist = await exists(verifiedFullPath); 11 | 12 | if (!doesTargetExist) { 13 | throw new Error(`"${verifiedFullPath}" file or directory does not exist.`); 14 | } 15 | 16 | return deleteRecord(verifiedFullPath); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-file-instance/remove-file-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { IRemoveFileInstanceProps } from './remove-file-instance.types'; 4 | 5 | export const removeFileInstance = 6 | ({ deleteRecord, isFile, rootDirectoryName }: IRemoveFileInstanceProps) => 7 | async (fullPath: string): Promise => { 8 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 9 | 10 | const targetIsOfTypeFile = await isFile(verifiedFullPath); 11 | 12 | if (!targetIsOfTypeFile) { 13 | throw new Error(`"${verifiedFullPath}" is not a file.`); 14 | } 15 | 16 | return deleteRecord(verifiedFullPath); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/database/open-indexeddb-connection/open-indexeddb-connection.spec.ts: -------------------------------------------------------------------------------- 1 | import { openIndexedDBConnection } from '@database'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | describe('openIndexedDBConnection Function', () => { 6 | functionImportTest(openIndexedDBConnection); 7 | 8 | it('should open indexedDB connection', () => { 9 | expect(openIndexedDBConnection('databaseName')).toEqual({ 10 | source: null, 11 | _error: null, 12 | _result: null, 13 | listeners: [], 14 | onerror: null, 15 | onsuccess: null, 16 | onblocked: null, 17 | transaction: null, 18 | onupgradeneeded: null, 19 | readyState: 'pending', 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/utils/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './is-string/is-string.function'; 2 | export * from './is-function/is-function.function'; 3 | export * from './is-valid-path/is-valid-path.function'; 4 | export * from './try-catch-wrapper/try-catch-wrapper.function'; 5 | export * from './starts-with-slash/starts-with-slash.function'; 6 | export * from './get-directory-name/get-directory-name.function'; 7 | export * from './validate-create-fs-props/validate-create-fs-props.function'; 8 | export * from './has-root-directory-prefix/has-root-directory-prefix.function'; 9 | export * from './with-root-directory-prefix/with-root-directory-prefix.function'; 10 | export * from './format-and-validate-full-path/format-and-validate-full-path.function'; 11 | -------------------------------------------------------------------------------- /scripts/git-push-changes.sh: -------------------------------------------------------------------------------- 1 | commitMessage="$1" 2 | 3 | currentBranch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') 4 | developBranchName=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p') 5 | 6 | if [ $currentBranch == $developBranchName ]; then 7 | echo 'Error: cannot push changes to '$developBranchName'.' 8 | 9 | exit 1 10 | fi 11 | 12 | if [ -z "$commitMessage" ]; then 13 | echo 'Error: commit message was not specified.' 14 | 15 | exit 1 16 | fi 17 | 18 | git add . 19 | echo "Info: Staged all files." 20 | 21 | git commit -m "$commitMessage" 22 | echo "Info: Added the commit with message: '$commitMessage'." 23 | 24 | git push origin "$currentBranch" 25 | echo "Info: Push changes to '$currentBranch' branch." 26 | -------------------------------------------------------------------------------- /lib/framework/parts/create-root-directory-instance/create-root-directory-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { EEntryType, IDirectoryEntry } from '@types'; 2 | 3 | import { ICreateRootDirectoryInstanceProps } from './create-root-directory-instance.types'; 4 | 5 | export const createRootDirectoryInstance = 6 | ({ putRecord, rootDirectoryName }: ICreateRootDirectoryInstanceProps) => 7 | async (): Promise => { 8 | const entry: IDirectoryEntry = { 9 | isRoot: true, 10 | createdAt: Date.now(), 11 | name: rootDirectoryName, 12 | type: EEntryType.DIRECTORY, 13 | fullPath: rootDirectoryName, 14 | directory: rootDirectoryName, 15 | }; 16 | 17 | return putRecord(entry); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/database/initialize-object-store-instance/initialize-object-store-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { initializeDatabase } from '@database'; 2 | 3 | import { IInitializeObjectStoreInstanceProps } from './initialize-object-store-instance.types'; 4 | 5 | export const initializeObjectStoreInstance = 6 | ({ databaseName, databaseVersion, objectStoreName }: IInitializeObjectStoreInstanceProps) => 7 | async (type: IDBTransactionMode): Promise => { 8 | const database = await initializeDatabase({ 9 | databaseName, 10 | databaseVersion, 11 | objectStoreName, 12 | }); 13 | 14 | const transaction = database.transaction(objectStoreName, type); 15 | 16 | return transaction.objectStore(objectStoreName); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/database/initialize-database/initialize-database.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest } from '@utils'; 2 | 3 | import { initializeDatabase } from './initialize-database.function'; 4 | 5 | describe('initializeDatabase Function', () => { 6 | functionImportTest(initializeDatabase); 7 | 8 | it('should initialize database', async () => { 9 | const database = await initializeDatabase({ 10 | databaseVersion: 1, 11 | databaseName: 'databaseName', 12 | objectStoreName: 'objectStoreName', 13 | }); 14 | 15 | expect(Object.keys(database)).toHaveLength(8); 16 | expect(database.version).toEqual(1); 17 | expect(database.name).toEqual('databaseName'); 18 | expect(database.objectStoreNames).toEqual(['objectStoreName']); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/utils/functions/is-function/is-function.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, isFunction } from '@utils'; 2 | 3 | describe('isFunction Function', () => { 4 | functionImportTest(isFunction); 5 | 6 | it('should return false when provided parameter is not a function', () => { 7 | expect(isFunction({})).toBeFalsy(); 8 | expect(isFunction(null)).toBeFalsy(); 9 | expect(isFunction('A123')).toBeFalsy(); 10 | expect(isFunction(undefined)).toBeFalsy(); 11 | expect(isFunction([1, 2, 3])).toBeFalsy(); 12 | expect(isFunction(new Date())).toBeFalsy(); 13 | }); 14 | 15 | it('should return true when provided parameter is a function', () => { 16 | expect(isFunction(() => {})).toBeTruthy(); 17 | expect(isFunction(jest.fn())).toBeTruthy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/utils/functions/is-string/is-string.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, isString } from '@utils'; 2 | 3 | describe('isString Function', () => { 4 | functionImportTest(isString); 5 | 6 | it('should return false when provided parameter is not a string value', () => { 7 | expect(isString({})).toBeFalsy(); 8 | expect(isString(null)).toBeFalsy(); 9 | expect(isString(-500)).toBeFalsy(); 10 | expect(isString(undefined)).toBeFalsy(); 11 | expect(isString([1, 2, 3])).toBeFalsy(); 12 | expect(isString(new Date())).toBeFalsy(); 13 | }); 14 | 15 | it('should return true when provided parameter is a string value', () => { 16 | expect(isString('')).toBeTruthy(); 17 | expect(isString('123')).toBeTruthy(); 18 | expect(isString(String('test'))).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /lib/framework/parts/is-file-instance/is-file-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { EEntryType } from '@types'; 4 | 5 | import { IIsFileInstanceProps } from './is-file-instance.types'; 6 | 7 | const onResolve = ({ result }: IDBRequest) => Boolean(result?.type === EEntryType.FILE); 8 | 9 | export const isFileInstance = 10 | ({ exists, getRecord, rootDirectoryName }: IIsFileInstanceProps) => 11 | async (fullPath: string): Promise => { 12 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 13 | 14 | const doesFileExist = await exists(verifiedFullPath); 15 | 16 | if (!doesFileExist) { 17 | throw new Error(`"${verifiedFullPath}" file does not exist.`); 18 | } 19 | 20 | return getRecord(verifiedFullPath, onResolve); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/database/open-cursor-instance/open-cursor-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDatabaseCrud } from '@database'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { openCursor, putRecord } = getDatabaseCrud({ 6 | databaseVersion: 1, 7 | databaseName: 'openCursor', 8 | objectStoreName: 'objectStoreName', 9 | }); 10 | 11 | const onResolve = ({ result }: IDBRequest, resolve: (value: unknown) => void) => resolve(result.value); 12 | 13 | describe('openCursor Function', () => { 14 | functionImportTest(openCursor); 15 | 16 | it('should get record from database by cursor', async () => { 17 | await putRecord({ fullPath: 'fullPath', directory: 'directory' }); 18 | 19 | await expect(openCursor('directory', onResolve)).resolves.toEqual({ 20 | fullPath: 'fullPath', 21 | directory: 'directory', 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/framework/parts/file-details-instance/file-details-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { IFileEntry } from '@types'; 4 | 5 | import { IFileDetailsInstanceProps } from './file-details-instance.types'; 6 | 7 | const onResolve = ({ result }: IDBRequest) => result; 8 | 9 | export const fileDetailsInstance = 10 | ({ getRecord, isFile, rootDirectoryName }: IFileDetailsInstanceProps) => 11 | async (fullPath: string): Promise> => { 12 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 13 | 14 | const targetIsOfTypeFile = await isFile(verifiedFullPath); 15 | 16 | if (!targetIsOfTypeFile) { 17 | throw new Error(`"${verifiedFullPath}" is not a file.`); 18 | } 19 | 20 | return getRecord(verifiedFullPath, onResolve); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/framework/parts/is-directory-instance/is-directory-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { EEntryType } from '@types'; 4 | 5 | import { IIsDirectoryInstanceProps } from './is-directory-instance.types'; 6 | 7 | const onResolve = ({ result }: IDBRequest) => Boolean(result?.type === EEntryType.DIRECTORY); 8 | 9 | export const isDirectoryInstance = 10 | ({ exists, getRecord, rootDirectoryName }: IIsDirectoryInstanceProps) => 11 | async (fullPath: string): Promise => { 12 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 13 | 14 | const doesDirectoryExists = await exists(verifiedFullPath); 15 | 16 | if (!doesDirectoryExists) { 17 | throw new Error(`"${verifiedFullPath}" directory does not exist.`); 18 | } 19 | 20 | return getRecord(verifiedFullPath, onResolve); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/framework/parts/directory-details-instance/directory-details-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { IDirectoryEntry } from '@types'; 4 | 5 | import { IDirectoryDetailsInstanceProps } from './directory-details-instance.types'; 6 | 7 | const onResolve = ({ result }: IDBRequest) => result; 8 | 9 | export const directoryDetailsInstance = 10 | ({ getRecord, isDirectory, rootDirectoryName }: IDirectoryDetailsInstanceProps) => 11 | async (fullPath: string): Promise => { 12 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 13 | 14 | const targetIsOfTypeDirectory = await isDirectory(fullPath); 15 | 16 | if (!targetIsOfTypeDirectory) { 17 | throw new Error(`"${verifiedFullPath}" is not a directory.`); 18 | } 19 | 20 | return getRecord(verifiedFullPath, onResolve); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/utils/functions/validate-create-fs-props/validate-create-fs-props.schema.ts: -------------------------------------------------------------------------------- 1 | const stringRegExp = '^[a-zA-Z0-9_.-]*$'; 2 | 3 | export const createFsPropsSchema = { 4 | type: 'object', 5 | id: '/CreateFsPropsSchema', 6 | properties: { 7 | databaseName: { 8 | minLength: 4, 9 | maxLength: 50, 10 | type: 'string', 11 | pattern: stringRegExp, 12 | }, 13 | objectStoreName: { 14 | minLength: 1, 15 | maxLength: 20, 16 | type: 'string', 17 | pattern: stringRegExp, 18 | }, 19 | rootDirectoryName: { 20 | minLength: 1, 21 | maxLength: 20, 22 | type: 'string', 23 | pattern: stringRegExp, 24 | }, 25 | databaseVersion: { 26 | minimum: 1, 27 | maximum: 100, 28 | type: 'integer', 29 | }, 30 | }, 31 | required: ['databaseName', 'databaseVersion', 'objectStoreName', 'rootDirectoryName'], 32 | }; 33 | -------------------------------------------------------------------------------- /lib/utils/functions/try-catch-wrapper/try-catch-wrapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, tryCatchWrapper } from '@utils'; 2 | 3 | const promiseResolveCallback = () => 4 | new Promise((resolve) => { 5 | resolve(5); 6 | }); 7 | 8 | const promiseRejectCallback = () => 9 | new Promise((_, reject) => { 10 | reject(new Error('test error')); 11 | 12 | throw new Error('test error'); 13 | }); 14 | 15 | describe('tryCatchWrapper Function', () => { 16 | functionImportTest(tryCatchWrapper); 17 | 18 | it('should call onError callback when an error occurs', async () => { 19 | const onError = jest.fn(); 20 | 21 | await tryCatchWrapper(promiseRejectCallback, onError); 22 | 23 | expect(onError).toHaveBeenCalled(); 24 | }); 25 | 26 | it('should resolve promise when an error does not occur', async () => { 27 | await expect(tryCatchWrapper(promiseResolveCallback)).resolves.toEqual(5); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /lib/database/delete-record-instance/delete-record-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDatabaseCrud } from '@database'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { deleteRecord, getRecord, putRecord } = getDatabaseCrud({ 6 | databaseVersion: 1, 7 | databaseName: 'deleteRecord', 8 | objectStoreName: 'objectStoreName', 9 | }); 10 | 11 | describe('deleteRecord Function', () => { 12 | functionImportTest(deleteRecord); 13 | 14 | it('should delete record from database', async () => { 15 | await putRecord({ fullPath: 'fullPath', directory: 'directory' }); 16 | 17 | await expect(getRecord('fullPath', ({ result }: IDBRequest) => result)).resolves.toEqual({ 18 | fullPath: 'fullPath', 19 | directory: 'directory', 20 | }); 21 | 22 | await deleteRecord('fullPath'); 23 | 24 | await expect(getRecord('fullPath', ({ result }: IDBRequest) => result)).resolves.toBeUndefined(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/database/open-cursor-instance/open-cursor-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { OBJECT_STORE_INDEX_NAME } from '@constants'; 2 | 3 | import { IOpenCursorInstanceProps } from './open-cursor-instance.types'; 4 | 5 | export const openCursorInstance = 6 | ({ initializeObjectStore }: IOpenCursorInstanceProps) => 7 | async ( 8 | value: unknown, 9 | onResolve: (target: IDBRequest, resolve: (value: TValue) => void) => TValue, 10 | ): Promise => { 11 | const objectStore = await initializeObjectStore('readonly'); 12 | const objectStoreIndex = objectStore.index(OBJECT_STORE_INDEX_NAME); 13 | 14 | const keyRange = IDBKeyRange.only(value); 15 | const request = objectStoreIndex.openCursor(keyRange); 16 | 17 | return new Promise((resolve, reject) => { 18 | request.onerror = reject; 19 | 20 | request.onsuccess = ({ target }: Event) => onResolve(target as IDBRequest, resolve); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/database/get-record-instance/get-record-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDatabaseCrud } from '@database'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { getRecord, putRecord } = getDatabaseCrud({ 6 | databaseVersion: 1, 7 | databaseName: 'getRecord', 8 | objectStoreName: 'objectStoreName', 9 | }); 10 | 11 | describe('getRecord Function', () => { 12 | functionImportTest(getRecord); 13 | 14 | it('should return undefined when target does not exist', async () => { 15 | await expect(getRecord('xD', ({ result }) => result)).resolves.toBeUndefined(); 16 | }); 17 | 18 | it('should return added record into the database', async () => { 19 | await putRecord({ fullPath: 'fullPath', directory: 'directory' }); 20 | 21 | await expect(getRecord('fullPath', ({ result }: IDBRequest) => result)).resolves.toEqual({ 22 | fullPath: 'fullPath', 23 | directory: 'directory', 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compileOnSave": true, 4 | "include": ["./lib"], 5 | "exclude": ["dist", "node_modules"], 6 | "compilerOptions": { 7 | "target": "es5", 8 | "module": "ESNext", 9 | "allowJs": true, 10 | "declaration": true, 11 | "outDir": "dist", 12 | "rootDir": "lib", 13 | "removeComments": true, 14 | "strict": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "strictBindCallApply": true, 19 | "strictPropertyInitialization": true, 20 | "noImplicitThis": true, 21 | "alwaysStrict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "moduleResolution": "node", 27 | "esModuleInterop": true, 28 | "skipLibCheck": true, 29 | "forceConsistentCasingInFileNames": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/database/put-record-instance/put-record-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDatabaseCrud } from '@database'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { getRecord, putRecord } = getDatabaseCrud({ 6 | databaseVersion: 1, 7 | databaseName: 'putRecord', 8 | objectStoreName: 'objectStoreName', 9 | }); 10 | 11 | describe('putRecord Function', () => { 12 | functionImportTest(putRecord); 13 | 14 | it('should throw an error when the operation does not meet requirements', async () => { 15 | await expect(putRecord('xD')).rejects.toThrow('Data provided to an operation does not meet requirements.'); 16 | }); 17 | 18 | it('should return added record into the database', async () => { 19 | await putRecord({ fullPath: 'fullPath', directory: 'directory' }); 20 | 21 | await expect(getRecord('fullPath', ({ result }: IDBRequest) => result)).resolves.toEqual({ 22 | fullPath: 'fullPath', 23 | directory: 'directory', 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | push: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14.x, 16.x, 18.x] 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 1 15 | 16 | - name: Setup node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: "${{ matrix.node-version }}" 20 | cache: "yarn" 21 | 22 | - uses: actions/cache@v2 23 | with: 24 | path: "~/.cache/yarn" 25 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 26 | 27 | - name: Install dependencies 28 | run: yarn install 29 | 30 | - name: Typecheck, format check, and lint 31 | run: | 32 | yarn typecheck & 33 | yarn format:check & 34 | yarn lint & 35 | wait 36 | 37 | - name: Test 38 | run: yarn test:ci 39 | 40 | - name: Build 41 | run: yarn build 42 | -------------------------------------------------------------------------------- /lib/database/get-database-crud/get-database-crud.function.ts: -------------------------------------------------------------------------------- 1 | import { 2 | deleteRecordInstance, 3 | getRecordInstance, 4 | initializeObjectStoreInstance, 5 | openCursorInstance, 6 | putRecordInstance, 7 | } from '@database'; 8 | 9 | import { IGetDatabaseCrudOutput, IGetDatabaseCrudProps } from './get-database-crud.types'; 10 | 11 | export const getDatabaseCrud = ({ 12 | databaseName, 13 | databaseVersion, 14 | objectStoreName, 15 | }: IGetDatabaseCrudProps): IGetDatabaseCrudOutput => { 16 | const initializeObjectStore = initializeObjectStoreInstance({ 17 | databaseName, 18 | databaseVersion, 19 | objectStoreName, 20 | }); 21 | 22 | const getRecord = getRecordInstance({ initializeObjectStore }); 23 | const putRecord = putRecordInstance({ initializeObjectStore }); 24 | const openCursor = openCursorInstance({ initializeObjectStore }); 25 | const deleteRecord = deleteRecordInstance({ initializeObjectStore }); 26 | 27 | return { getRecord, putRecord, openCursor, deleteRecord }; 28 | }; 29 | -------------------------------------------------------------------------------- /lib/utils/functions/with-root-directory-prefix/with-root-directory-prefix.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, withRootDirectoryPrefix } from '@utils'; 2 | 3 | describe('withRootDirectoryPrefix Function', () => { 4 | functionImportTest(withRootDirectoryPrefix); 5 | 6 | it('should return rootDirectoryName when fullPath is an empty string', () => { 7 | expect(withRootDirectoryPrefix('', 'root')).toEqual('root'); 8 | }); 9 | 10 | it('should return passed fullPath when it contains rootDirectoryName', () => { 11 | expect(withRootDirectoryPrefix('root/test', 'root')).toEqual('root/test'); 12 | }); 13 | 14 | it('should add rootDirectoryName to fullPath when it does not contain it', () => { 15 | expect(withRootDirectoryPrefix('root/test', 'root')).toEqual('root/test'); 16 | expect(withRootDirectoryPrefix('oot/test', 'root')).toEqual('root/oot/test'); 17 | }); 18 | 19 | it('should add rootDirectoryName with slash to fullPath when it does not contain it', () => { 20 | expect(withRootDirectoryPrefix('test', 'root')).toEqual('root/test'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/utils/functions/format-and-validate-full-path/format-and-validate-full-path.function.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@utils'; 2 | 3 | import { IS_VALID_PATH_REG_EXP_STRING } from '@constants'; 4 | 5 | import { isValidPath, withRootDirectoryPrefix } from '..'; 6 | 7 | export function formatAndValidateFullPath(fullPath: string, rootDirectoryName?: string): string { 8 | if (!isString(rootDirectoryName) || rootDirectoryName === '') { 9 | throw new Error('rootDirectoryName parameter was not provided'); 10 | } 11 | 12 | if (!isString(fullPath)) { 13 | throw new Error('fullPath parameter was not provided'); 14 | } 15 | 16 | if (fullPath === rootDirectoryName) { 17 | return rootDirectoryName; 18 | } 19 | 20 | const fullPathWithPrefix = withRootDirectoryPrefix(fullPath, rootDirectoryName); 21 | 22 | if (!fullPathWithPrefix || !isValidPath(fullPathWithPrefix)) { 23 | throw new Error( 24 | `"${fullPath}" path is invalid. Path must match the following pattern: /${IS_VALID_PATH_REG_EXP_STRING}/`, 25 | ); 26 | } 27 | 28 | return fullPathWithPrefix; 29 | } 30 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-instance/remove-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { exists, remove, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'remove', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('remove Function', () => { 13 | functionImportTest(remove); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(remove('test//test2 ')).rejects.toThrow('"test//test2 " path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the user wants to delete a file that does not exist', async () => { 20 | await expect(remove('file.txt')).rejects.toThrow('"root/file.txt" file or directory does not exist.'); 21 | }); 22 | 23 | it('should remove created file in root directory', async () => { 24 | await writeFile('file1.txt', 'test content'); 25 | 26 | await remove('file1.txt'); 27 | await expect(exists('file1.txt')).resolves.toBeFalsy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Paweł Wojtasiński 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/utils/functions/validate-create-fs-props/validate-create-fs-props.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, toMatchSnapshot } from '@utils'; 2 | 3 | import { validateCreateFsProps } from './validate-create-fs-props.function'; 4 | import { createFsPropsSchema } from './validate-create-fs-props.schema'; 5 | 6 | describe('validateCreateFsProps Function', () => { 7 | functionImportTest(validateCreateFsProps); 8 | toMatchSnapshot(() => createFsPropsSchema); 9 | 10 | it('should throw an error when passed value is not an object', () => { 11 | // @ts-expect-error 12 | expect(() => validateCreateFsProps(null)).toThrowErrorMatchingSnapshot(); 13 | }); 14 | 15 | it('should require all object fields', () => { 16 | // @ts-expect-error 17 | expect(() => validateCreateFsProps({})).toThrowErrorMatchingSnapshot(); 18 | }); 19 | 20 | it('should validate passed value', () => { 21 | const props = { 22 | databaseName: 'd', 23 | databaseVersion: 1.1, 24 | rootDirectoryName: ' start_with_string', 25 | objectStoreName: '_contain_string_contain_string_contain_string_contain_string', 26 | }; 27 | 28 | expect(() => validateCreateFsProps(props)).toThrowErrorMatchingSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/framework/parts/exists-instance/exists-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, exists, remove, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'exists', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('exists Function', () => { 13 | functionImportTest(exists); 14 | 15 | it('should check if directory exists', async () => { 16 | await expect(exists('test')).resolves.toBeFalsy(); 17 | 18 | await createDirectory('test'); 19 | await expect(exists('tes')).resolves.toBeFalsy(); 20 | await expect(exists('test')).resolves.toBeTruthy(); 21 | }); 22 | 23 | it('should check if file exists', async () => { 24 | await expect(exists('file.txt')).resolves.toBeFalsy(); 25 | 26 | await writeFile('file.txt', 'test'); 27 | await expect(exists('file.tx')).resolves.toBeFalsy(); 28 | await expect(exists('file.txt')).resolves.toBeTruthy(); 29 | await expect(exists('test/file.tx')).resolves.toBeFalsy(); 30 | 31 | await remove('file.txt'); 32 | await expect(exists('file.txt')).resolves.toBeFalsy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /lib/framework/parts/details-instance/details-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { IDirectoryEntry, IFileEntry } from '@types'; 4 | 5 | import { IDetailsInstanceProps } from './details-instance.types'; 6 | 7 | export const detailsInstance = 8 | ({ directoryDetails, exists, fileDetails, isDirectory, isFile, rootDirectoryName }: IDetailsInstanceProps) => 9 | async (fullPath: string): Promise | IDirectoryEntry> => { 10 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 11 | 12 | const doesTargetExist = await exists(verifiedFullPath); 13 | 14 | if (!doesTargetExist) { 15 | throw new Error(`"${verifiedFullPath}" file or directory does not exist.`); 16 | } 17 | 18 | const targetIsOfTypeFile = await isFile(verifiedFullPath); 19 | const targetIsOfTypeDirectory = await isDirectory(verifiedFullPath); 20 | 21 | if (targetIsOfTypeFile && targetIsOfTypeDirectory) { 22 | throw new Error(`"${verifiedFullPath}" is a path of file and directory.`); 23 | } 24 | 25 | if (targetIsOfTypeFile) { 26 | return fileDetails(verifiedFullPath); 27 | } 28 | 29 | return directoryDetails(verifiedFullPath); 30 | }; 31 | -------------------------------------------------------------------------------- /lib/database/initialize-object-store-instance/initialize-object-store-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest } from '@utils'; 2 | 3 | import { initializeObjectStoreInstance } from './initialize-object-store-instance.function'; 4 | 5 | describe('initializeObjectStoreInstance Function', () => { 6 | functionImportTest(initializeObjectStoreInstance); 7 | 8 | it('should return nested function to call', () => { 9 | const initializeObjectStore = initializeObjectStoreInstance({ 10 | databaseVersion: 1, 11 | databaseName: 'databaseName', 12 | objectStoreName: 'objectStoreName', 13 | }); 14 | 15 | expect(typeof initializeObjectStore).toEqual('function'); 16 | }); 17 | 18 | it('should initialize object store', async () => { 19 | const initializeObjectStore = initializeObjectStoreInstance({ 20 | databaseVersion: 1, 21 | databaseName: 'databaseName', 22 | objectStoreName: 'objectStoreName', 23 | }); 24 | 25 | const objectStore = await initializeObjectStore('readonly'); 26 | 27 | expect(Object.keys(objectStore)).toHaveLength(7); 28 | expect(objectStore.keyPath).toEqual('fullPath'); 29 | expect(objectStore.indexNames).toEqual(['directory']); 30 | expect(objectStore.transaction.mode).toEqual('readonly'); 31 | expect(objectStore.transaction.objectStoreNames).toEqual(['objectStoreName']); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /lib/framework/parts/is-file-instance/is-file-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, isFile, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'isFile', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('isFile Function', () => { 13 | functionImportTest(isFile); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(isFile('root/test/')).rejects.toThrow('"root/test/" path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the user wants to check a file that does not exist', async () => { 20 | await expect(isFile('file.txt')).rejects.toThrow('"root/file.txt" file does not exist.'); 21 | }); 22 | 23 | it('should pass those scenarios', async () => { 24 | await createDirectory('files'); 25 | await createDirectory('directories'); 26 | 27 | await expect(isFile('files')).resolves.toBeFalsy(); 28 | await expect(isFile('directories')).resolves.toBeFalsy(); 29 | 30 | await writeFile('file', 'content'); 31 | await expect(isFile('file')).resolves.toBeTruthy(); 32 | 33 | await writeFile('files/file', 'content'); 34 | await expect(isFile('files/file')).resolves.toBeTruthy(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/utils/functions/starts-with-slash/starts-with-slash.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, startsWithSlash } from '@utils'; 2 | 3 | describe('startsWithSlash Function', () => { 4 | functionImportTest(startsWithSlash); 5 | 6 | it('should return false when fullPath parameter is not a string value', () => { 7 | // @ts-expect-error 8 | expect(startsWithSlash({})).toBeFalsy(); 9 | 10 | // @ts-expect-error 11 | expect(startsWithSlash(null)).toBeFalsy(); 12 | 13 | // @ts-expect-error 14 | expect(startsWithSlash(-500)).toBeFalsy(); 15 | 16 | // @ts-expect-error 17 | expect(startsWithSlash(undefined)).toBeFalsy(); 18 | 19 | // @ts-expect-error 20 | expect(startsWithSlash([1, 2, 3])).toBeFalsy(); 21 | 22 | // @ts-expect-error 23 | expect(startsWithSlash(new Date())).toBeFalsy(); 24 | }); 25 | 26 | it('should return false when passed value is an empty string', () => { 27 | expect(startsWithSlash('')).toBeFalsy(); 28 | }); 29 | 30 | it('should return false when passed value does not start with slash', () => { 31 | expect(startsWithSlash('123')).toBeFalsy(); 32 | expect(startsWithSlash('test')).toBeFalsy(); 33 | }); 34 | 35 | it('should return true when passed value starts with slash', () => { 36 | expect(startsWithSlash('/')).toBeTruthy(); 37 | expect(startsWithSlash('/test')).toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /lib/framework/parts/rename-file-instance/rename-file-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath, getDirectoryName } from '@utils'; 2 | 3 | import { IFileEntry } from '@types'; 4 | 5 | import { IRenameFileInstanceProps } from './rename-file-instance.types'; 6 | 7 | export const renameFileInstance = 8 | ({ exists, isFile, removeFile, rootDirectoryName, updateFileDetails }: IRenameFileInstanceProps) => 9 | async (fullPath: string, newFilename: string): Promise> => { 10 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 11 | 12 | const sourceIsOfTypeFile = await isFile(verifiedFullPath); 13 | 14 | if (!sourceIsOfTypeFile) { 15 | throw new Error(`"${verifiedFullPath}" is not a file.`); 16 | } 17 | 18 | const pathDirectory = getDirectoryName(verifiedFullPath, rootDirectoryName); 19 | const newFullPath = `${pathDirectory}/${newFilename}`; 20 | 21 | const newFullPathIsAlreadyTaken = await exists(newFullPath); 22 | 23 | if (newFullPathIsAlreadyTaken) { 24 | throw new Error(`"${newFullPath}" is already taken.`); 25 | } 26 | 27 | const updatedFileDetails = await updateFileDetails(verifiedFullPath, { 28 | name: newFilename, 29 | fullPath: newFullPath, 30 | }); 31 | 32 | await removeFile(verifiedFullPath); 33 | 34 | return updatedFileDetails; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/framework/parts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exists-instance/exists-instance.function'; 2 | export * from './remove-instance/remove-instance.function'; 3 | export * from './details-instance/details-instance.function'; 4 | export * from './is-file-instance/is-file-instance.function'; 5 | export * from './copy-file-instance/copy-file-instance.function'; 6 | export * from './read-file-instance/read-file-instance.function'; 7 | export * from './move-file-instance/move-file-instance.function'; 8 | export * from './write-file-instance/write-file-instance.function'; 9 | export * from './rename-file-instance/rename-file-instance.function'; 10 | export * from './remove-file-instance/remove-file-instance.function'; 11 | export * from './is-directory-instance/is-directory-instance.function'; 12 | export * from './file-details-instance/file-details-instance.function'; 13 | export * from './read-directory-instance/read-directory-instance.function'; 14 | export * from './create-directory-instance/create-directory-instance.function'; 15 | export * from './remove-directory-instance/remove-directory-instance.function'; 16 | export * from './directory-details-instance/directory-details-instance.function'; 17 | export * from './update-file-details-instance/update-file-details-instance.function'; 18 | export * from './create-root-directory-instance/create-root-directory-instance.function'; 19 | 20 | export * from './read-directory-instance/read-directory-instance.types'; 21 | -------------------------------------------------------------------------------- /lib/framework/parts/is-directory-instance/is-directory-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, isDirectory, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'isDirectory', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('isDirectory Function', () => { 13 | functionImportTest(isDirectory); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(isDirectory('root/test/')).rejects.toThrow('"root/test/" path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the user wants to check a directory that does not exist', async () => { 20 | await expect(isDirectory('directory')).rejects.toThrow('"root/directory" directory does not exist.'); 21 | }); 22 | 23 | it('should pass those scenarios', async () => { 24 | await createDirectory('files'); 25 | await createDirectory('directories'); 26 | 27 | await expect(isDirectory('files')).resolves.toBeTruthy(); 28 | await expect(isDirectory('directories')).resolves.toBeTruthy(); 29 | 30 | await writeFile('file', 'content'); 31 | await expect(isDirectory('file')).resolves.toBeFalsy(); 32 | 33 | await writeFile('files/file', 'content'); 34 | await expect(isDirectory('files/file')).resolves.toBeFalsy(); 35 | await expect(isDirectory('files')).resolves.toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /lib/framework/parts/read-file-instance/read-file-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, exists, readFile, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'readFile', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('readFile Function', () => { 13 | functionImportTest(readFile); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(readFile('test//test2 ')).rejects.toThrow('"test//test2 " path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the file does not exist', async () => { 20 | await expect(readFile('file.txt')).rejects.toThrow('"root/file.txt" file does not exist.'); 21 | await expect(readFile('test/file.txt')).rejects.toThrow('"root/test/file.txt" file does not exist.'); 22 | }); 23 | 24 | it('should throw type error when selected target is not a file', async () => { 25 | await createDirectory('directory_as_a_file'); 26 | await expect(exists('directory_as_a_file')).resolves.toBeTruthy(); 27 | 28 | await expect(readFile('directory_as_a_file')).rejects.toThrow('"root/directory_as_a_file" is not a file.'); 29 | }); 30 | 31 | it('should return content of found file', async () => { 32 | const file = await writeFile('file.txt', 'test 2 content'); 33 | 34 | await expect(readFile(file.fullPath)).resolves.toEqual('test 2 content'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/framework/parts/update-file-details-instance/update-file-details-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath, getDirectoryName, tryCatchWrapper } from '@utils'; 2 | 3 | import { IFileEntry } from '@types'; 4 | 5 | import { IUpdateFileDetailsInstanceProps } from './update-file-details-instance.types'; 6 | 7 | export const updateFileDetailsInstance = 8 | ({ fileDetails, isDirectory, putRecord, rootDirectoryName }: IUpdateFileDetailsInstanceProps) => 9 | async (fullPath: string, newFileEntry: Partial>): Promise> => { 10 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 11 | 12 | if (verifiedFullPath === rootDirectoryName) { 13 | throw new Error(`Root directory: "${verifiedFullPath}" cannot be updated.`); 14 | } 15 | 16 | const directory = getDirectoryName(verifiedFullPath, rootDirectoryName); 17 | const doesDirectoryExists = await isDirectory(directory); 18 | 19 | if (!doesDirectoryExists) { 20 | throw new Error(`"${directory}" is not a directory.`); 21 | } 22 | 23 | const targetIsTypeOfDirectory = await tryCatchWrapper( 24 | () => isDirectory(verifiedFullPath), 25 | () => false, 26 | ); 27 | 28 | if (targetIsTypeOfDirectory) { 29 | throw new Error(`"${verifiedFullPath}" you cannot update a directory.`); 30 | } 31 | 32 | const existFileDetails = await fileDetails(verifiedFullPath); 33 | const concatedFileDetails = { ...existFileDetails, ...newFileEntry }; 34 | 35 | // @ts-expect-error 36 | return putRecord(concatedFileDetails); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-directory-instance/remove-directory-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath, tryCatchWrapper } from '@utils'; 2 | 3 | import { IRemoveDirectoryInstanceProps } from './remove-directory-instance.types'; 4 | 5 | export const removeDirectoryInstance = ({ 6 | isDirectory, 7 | readDirectory, 8 | remove, 9 | rootDirectoryName, 10 | }: IRemoveDirectoryInstanceProps) => { 11 | async function removeNestedDirectory(fullPath: string): Promise { 12 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 13 | 14 | const { directories, directoriesCount, files, filesCount } = await readDirectory(verifiedFullPath); 15 | 16 | if (filesCount > 0) { 17 | for (const _file of files) { 18 | await remove(_file.fullPath); 19 | } 20 | } 21 | 22 | if (!directoriesCount) { 23 | await remove(fullPath); 24 | 25 | return; 26 | } 27 | 28 | for (const _directory of directories) { 29 | if (!_directory.isRoot) { 30 | await removeNestedDirectory(_directory.fullPath); 31 | } 32 | 33 | await tryCatchWrapper(() => remove(_directory.fullPath)); 34 | } 35 | } 36 | 37 | return async function removeDirectory(fullPath: string): Promise { 38 | const targetIsOfTypeDirectory = await isDirectory(fullPath); 39 | 40 | if (!targetIsOfTypeDirectory) { 41 | throw new Error(`"${fullPath}" is not a directory.`); 42 | } 43 | 44 | await removeNestedDirectory(fullPath); 45 | 46 | if (fullPath !== rootDirectoryName) { 47 | await tryCatchWrapper(() => remove(fullPath)); 48 | } 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/framework/parts/copy-file-instance/copy-file-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath, getDirectoryName } from '@utils'; 2 | 3 | import { IFileEntry } from '@types'; 4 | 5 | import { ICopyFileInstanceProps } from './copy-file-instance.types'; 6 | 7 | export const copyFileInstance = 8 | ({ exists, fileDetails, isDirectory, isFile, rootDirectoryName, writeFile }: ICopyFileInstanceProps) => 9 | async (sourcePath: string, destinationPath: string): Promise> => { 10 | const verifiedSourcePath = formatAndValidateFullPath(sourcePath, rootDirectoryName); 11 | const verifiedDestinationPath = formatAndValidateFullPath(destinationPath, rootDirectoryName); 12 | 13 | const sourceIsOfTypeFile = await isFile(verifiedSourcePath); 14 | 15 | if (!sourceIsOfTypeFile) { 16 | throw new Error(`"${verifiedSourcePath}" source is not a file.`); 17 | } 18 | 19 | const destinationDirectory = getDirectoryName(verifiedDestinationPath, rootDirectoryName); 20 | const destinationDirectoryIsOfTypeDirectory = await isDirectory(destinationDirectory); 21 | 22 | if (!destinationDirectoryIsOfTypeDirectory) { 23 | throw new Error(`"${destinationDirectory}" destination directory does not exist.`); 24 | } 25 | 26 | const destinationPathIsAlreadyTaken = await exists(verifiedDestinationPath); 27 | 28 | if (destinationPathIsAlreadyTaken) { 29 | throw new Error(`"${verifiedDestinationPath}" is already taken.`); 30 | } 31 | 32 | const sourceFileDetails = await fileDetails(verifiedSourcePath); 33 | 34 | return writeFile(verifiedDestinationPath, sourceFileDetails.data as TData); 35 | }; 36 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-file-instance/remove-file-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, exists, removeFile, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'removeFile', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('removeFile Function', () => { 13 | functionImportTest(removeFile); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(removeFile('test//test2 ')).rejects.toThrow('"test//test2 " path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the user wants to delete a file that does not exist', async () => { 20 | await expect(removeFile('file.txt')).rejects.toThrow('"root/file.txt" file does not exist.'); 21 | }); 22 | 23 | it('should throw an error when user tries to remove root directory', async () => { 24 | await expect(removeFile('root')).rejects.toThrow('"root" is not a file.'); 25 | }); 26 | 27 | it('should throw type error when selected target is not a file', async () => { 28 | await createDirectory('directory_as_a_file'); 29 | await expect(exists('directory_as_a_file')).resolves.toBeTruthy(); 30 | 31 | await expect(removeFile('directory_as_a_file')).rejects.toThrow('"root/directory_as_a_file" is not a file.'); 32 | }); 33 | 34 | it('should remove created file in root directory', async () => { 35 | await writeFile('file1.txt', 'test content'); 36 | 37 | await removeFile('file1.txt'); 38 | await expect(exists('file1.txt')).resolves.toBeFalsy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /lib/framework/parts/write-file-instance/write-file-instance.function.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { formatAndValidateFullPath, getDirectoryName, tryCatchWrapper } from '@utils'; 4 | 5 | import { EEntryType, IFileEntry } from '@types'; 6 | 7 | import { IWriteFileInstanceProps } from './write-file-instance.types'; 8 | 9 | export const writeFileInstance = 10 | ({ isDirectory, putRecord, rootDirectoryName }: IWriteFileInstanceProps) => 11 | async (fullPath: string, data: TData): Promise> => { 12 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 13 | 14 | if (verifiedFullPath === rootDirectoryName) { 15 | throw new Error(`Root directory: "${verifiedFullPath}" cannot be a file.`); 16 | } 17 | 18 | const basename = path.basename(verifiedFullPath); 19 | const directory = getDirectoryName(verifiedFullPath, rootDirectoryName); 20 | 21 | const doesDirectoryExists = await isDirectory(directory); 22 | 23 | if (!doesDirectoryExists) { 24 | throw new Error(`"${directory}" directory does not exist.`); 25 | } 26 | 27 | const targetIsTypeOfDirectory = await tryCatchWrapper( 28 | () => isDirectory(verifiedFullPath), 29 | () => false, 30 | ); 31 | 32 | if (targetIsTypeOfDirectory) { 33 | throw new Error(`"${verifiedFullPath}" you cannot create a file with the same name as the directory.`); 34 | } 35 | 36 | const entry: IFileEntry = { 37 | data, 38 | directory, 39 | name: basename, 40 | type: EEntryType.FILE, 41 | createdAt: Date.now(), 42 | fullPath: verifiedFullPath, 43 | }; 44 | 45 | return putRecord(entry); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/framework/parts/create-directory-instance/create-directory-instance.function.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { formatAndValidateFullPath, getDirectoryName, tryCatchWrapper } from '@utils'; 4 | 5 | import { EEntryType, IDirectoryEntry } from '@types'; 6 | 7 | import { ICreateDirectoryInstanceProps } from './create-directory-instance.types'; 8 | 9 | export const createDirectoryInstance = 10 | ({ isDirectory, isFile, putRecord, rootDirectoryName }: ICreateDirectoryInstanceProps) => 11 | async (fullPath: string): Promise => { 12 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 13 | 14 | if (verifiedFullPath === rootDirectoryName) { 15 | throw new Error(`Root directory: "${verifiedFullPath}" already exist.`); 16 | } 17 | 18 | const basename = path.basename(verifiedFullPath); 19 | const directory = getDirectoryName(verifiedFullPath, rootDirectoryName); 20 | 21 | const doesDirectoryExists = await isDirectory(directory); 22 | 23 | if (!doesDirectoryExists) { 24 | throw new Error(`"${directory}" is not a directory.`); 25 | } 26 | 27 | const targetIsTypeOfFile = await tryCatchWrapper( 28 | () => isFile(verifiedFullPath), 29 | () => false, 30 | ); 31 | 32 | if (targetIsTypeOfFile) { 33 | throw new Error(`"${verifiedFullPath}" you cannot create a directory with the same name as the file.`); 34 | } 35 | 36 | const entry: IDirectoryEntry = { 37 | directory, 38 | isRoot: false, 39 | name: basename, 40 | createdAt: Date.now(), 41 | type: EEntryType.DIRECTORY, 42 | fullPath: verifiedFullPath, 43 | }; 44 | 45 | return putRecord(entry); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/database/initialize-database/initialize-database.function.ts: -------------------------------------------------------------------------------- 1 | import { openIndexedDBConnection } from '@database'; 2 | 3 | import { OBJECT_STORE_INDEX_NAME, OBJECT_STORE_KEY_PATH } from '@constants'; 4 | 5 | import { IInitializeDatabaseProps } from './initialize-database.types'; 6 | 7 | function getDatabaseObjectFromTarget(target: EventTarget | null): IDBDatabase | null { 8 | if (!target) { 9 | return null; 10 | } 11 | 12 | const targetWithType = target as IDBOpenDBRequest; 13 | 14 | return targetWithType.result; 15 | } 16 | 17 | function throwDatabaseOpenError(reject: (reason: unknown) => void, database: IDBDatabase | null) { 18 | if (!database) { 19 | reject(new Error('Something went wrong and the database transaction was not opened.')); 20 | } 21 | } 22 | 23 | export const initializeDatabase = ({ 24 | databaseName, 25 | databaseVersion, 26 | objectStoreName, 27 | }: IInitializeDatabaseProps): Promise => 28 | new Promise((resolve, reject) => { 29 | const request = openIndexedDBConnection(databaseName, databaseVersion); 30 | 31 | request.onerror = reject; 32 | 33 | request.onsuccess = ({ target }: Event) => { 34 | const database = getDatabaseObjectFromTarget(target); 35 | 36 | throwDatabaseOpenError(reject, database); 37 | 38 | resolve(database as IDBDatabase); 39 | }; 40 | 41 | request.onupgradeneeded = ({ target }: IDBVersionChangeEvent) => { 42 | const database = getDatabaseObjectFromTarget(target); 43 | 44 | throwDatabaseOpenError(reject, database); 45 | 46 | const objectStore = database?.createObjectStore(objectStoreName, { 47 | keyPath: OBJECT_STORE_KEY_PATH, 48 | }); 49 | 50 | objectStore?.createIndex(OBJECT_STORE_INDEX_NAME, OBJECT_STORE_INDEX_NAME, { unique: false }); 51 | }; 52 | }); 53 | -------------------------------------------------------------------------------- /lib/framework/create-fs.types.ts: -------------------------------------------------------------------------------- 1 | import { IDirectoryEntry, IFileEntry } from '@types'; 2 | 3 | import { IReadDirectoryInstanceOutput } from './parts'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export type AnyFunction = (...args: any[]) => any; 7 | 8 | export interface ICreateFsProps { 9 | databaseName?: string; 10 | databaseVersion?: number; 11 | objectStoreName?: string; 12 | rootDirectoryName?: string; 13 | } 14 | 15 | export interface ICreateFsOutput { 16 | copyFile: (sourcePath: string, destinationPath: string) => Promise>; 17 | createDirectory: (fullPath: string) => Promise; 18 | databaseName: string; 19 | databaseVersion: number; 20 | details: (fullPath: string) => Promise | IDirectoryEntry>; 21 | directoryDetails: (fullPath: string) => Promise; 22 | exists: (fullPath: string) => Promise; 23 | fileDetails: (fullPath: string) => Promise>; 24 | isDirectory: (fullPath: string) => Promise; 25 | isFile: (fullPath: string) => Promise; 26 | moveFile: (sourcePath: string, destinationPath: string) => Promise>; 27 | objectStoreName: string; 28 | readDirectory: (fullPath: string) => Promise; 29 | readFile: (fullPath: string) => Promise; 30 | remove: (fullPath: string) => Promise; 31 | removeDirectory: (fullPath: string) => Promise; 32 | removeFile: (fullPath: string) => Promise; 33 | renameFile: (fullPath: string, newFilename: string) => Promise>; 34 | rootDirectoryName: string; 35 | writeFile: (fullPath: string, data: TData) => Promise>; 36 | } 37 | -------------------------------------------------------------------------------- /lib/utils/functions/format-and-validate-full-path/format-and-validate-full-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest } from '@utils'; 2 | 3 | import { formatAndValidateFullPath } from './format-and-validate-full-path.function'; 4 | 5 | describe('formatAndValidateFullPath Function', () => { 6 | functionImportTest(formatAndValidateFullPath); 7 | 8 | describe('rootDirectoryName parameter', () => { 9 | it('should throw an error when it is a falsy value', () => { 10 | // @ts-expect-error 11 | expect(() => formatAndValidateFullPath('fullPath', null)).toThrow('rootDirectoryName parameter was not provided'); 12 | }); 13 | 14 | it('should throw an error when it is an empty string', () => { 15 | expect(() => formatAndValidateFullPath('fullPath', '')).toThrow('rootDirectoryName parameter was not provided'); 16 | }); 17 | }); 18 | 19 | it('should throw an error when fullPath parameter is not a string value', () => { 20 | // @ts-expect-error 21 | expect(() => formatAndValidateFullPath(null, 'root')).toThrow('fullPath parameter was not provided'); 22 | }); 23 | 24 | it('should return fullPath with prefix when validation passed', () => { 25 | expect(formatAndValidateFullPath('dir1/filename.ext', 'root')).toEqual('root/dir1/filename.ext'); 26 | 27 | expect(formatAndValidateFullPath('dir1/dir2/filename.ext', 'root')).toEqual('root/dir1/dir2/filename.ext'); 28 | }); 29 | 30 | it('should throw an error when passed fullPath is invalid', () => { 31 | expect(() => formatAndValidateFullPath('', 'root')).toThrow('"" path is invalid.'); 32 | 33 | expect(() => formatAndValidateFullPath('//double_slash', 'root')).toThrow( 34 | '"//double_slash" path is invalid. Path must match the following pattern: /^([A-Za-z]:|[A-Za-z0-9_-]+(.[A-Za-z0-9_-]+)*)((/[A-Za-z0-9_.-]+)+)$/', 35 | ); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /lib/framework/parts/file-details-instance/file-details-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, exists, fileDetails, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'fileDetails', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('fileDetails Function', () => { 13 | functionImportTest(fileDetails); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(fileDetails('test//test2 ')).rejects.toThrow('"test//test2 " path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the file does not exist', async () => { 20 | await expect(fileDetails('file.txt')).rejects.toThrow('"root/file.txt" file does not exist.'); 21 | await expect(fileDetails('test/file.txt')).rejects.toThrow('"root/test/file.txt" file does not exist.'); 22 | }); 23 | 24 | it('should throw type error when selected target is not a file', async () => { 25 | await createDirectory('directory_as_a_file'); 26 | await expect(exists('directory_as_a_file')).resolves.toBeTruthy(); 27 | 28 | await expect(fileDetails('directory_as_a_file')).rejects.toThrow('"root/directory_as_a_file" is not a file.'); 29 | }); 30 | 31 | it('should return details about file', async () => { 32 | const createdFile = await writeFile('file.txt', 'test 2 content'); 33 | const createdFileDetails = await fileDetails(createdFile.fullPath); 34 | 35 | expect(createdFileDetails.type).toEqual('file'); 36 | expect(createdFileDetails.name).toEqual('file.txt'); 37 | expect(createdFileDetails.directory).toEqual('root'); 38 | expect(createdFileDetails.data).toEqual('test 2 content'); 39 | expect(createdFileDetails.fullPath).toEqual('root/file.txt'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /lib/framework/parts/read-directory-instance/read-directory-instance.function.ts: -------------------------------------------------------------------------------- 1 | import { formatAndValidateFullPath } from '@utils'; 2 | 3 | import { EEntryType, IDirectoryEntry, IFileEntry } from '@types'; 4 | 5 | import { IReadDirectoryInstanceOutput, IReadDirectoryInstanceProps } from './read-directory-instance.types'; 6 | 7 | export const readDirectoryInstance = 8 | ({ isDirectory, openCursor, rootDirectoryName }: IReadDirectoryInstanceProps) => 9 | async (fullPath: string): Promise => { 10 | const verifiedFullPath = formatAndValidateFullPath(fullPath, rootDirectoryName); 11 | 12 | const targetIsOfTypeDirectory = await isDirectory(fullPath); 13 | 14 | if (!targetIsOfTypeDirectory) { 15 | throw new Error(`"${verifiedFullPath}" is not a directory.`); 16 | } 17 | 18 | const foundFiles: IFileEntry[] = []; 19 | const foundDirectories: IDirectoryEntry[] = []; 20 | 21 | const onResolve = ({ result }: IDBRequest, resolve: (value: unknown) => void) => { 22 | if (result) { 23 | const { value } = result; 24 | 25 | if (value.type === EEntryType.FILE) { 26 | const { data, ...restProps } = value; 27 | 28 | foundFiles.push(restProps); 29 | } else if (value.type === EEntryType.DIRECTORY && !value.isRoot) { 30 | foundDirectories.push(value); 31 | } 32 | 33 | result.continue(); 34 | } else { 35 | const filesCount = foundFiles.length; 36 | const directoriesCount = foundDirectories.length; 37 | const isEmpty = filesCount === 0 && directoriesCount === 0; 38 | 39 | resolve({ 40 | isEmpty, 41 | filesCount, 42 | directoriesCount, 43 | files: foundFiles, 44 | directories: foundDirectories, 45 | }); 46 | } 47 | }; 48 | 49 | // @ts-expect-error 50 | return openCursor(verifiedFullPath, onResolve); 51 | }; 52 | -------------------------------------------------------------------------------- /lib/framework/parts/move-file-instance/move-file-instance.function.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { formatAndValidateFullPath, getDirectoryName } from '@utils'; 4 | 5 | import { IFileEntry } from '@types'; 6 | 7 | import { IMoveFileInstanceProps } from './move-file-instance.types'; 8 | 9 | export const moveFileInstance = 10 | ({ exists, isDirectory, isFile, removeFile, rootDirectoryName, updateFileDetails }: IMoveFileInstanceProps) => 11 | async (sourcePath: string, destinationPath: string): Promise> => { 12 | const verifiedSourcePath = formatAndValidateFullPath(sourcePath, rootDirectoryName); 13 | const verifiedDestinationPath = formatAndValidateFullPath(destinationPath, rootDirectoryName); 14 | 15 | const sourceIsOfTypeFile = await isFile(verifiedSourcePath); 16 | 17 | if (!sourceIsOfTypeFile) { 18 | throw new Error(`"${verifiedSourcePath}" source is not a file.`); 19 | } 20 | 21 | const destinationDirectory = getDirectoryName(verifiedDestinationPath, rootDirectoryName); 22 | const destinationDirectoryIsOfTypeDirectory = await isDirectory(destinationDirectory); 23 | 24 | if (!destinationDirectoryIsOfTypeDirectory) { 25 | throw new Error(`"${destinationDirectory}" destination directory does not exist.`); 26 | } 27 | 28 | const destinationPathIsAlreadyTaken = await exists(verifiedDestinationPath); 29 | 30 | if (destinationPathIsAlreadyTaken) { 31 | throw new Error(`"${verifiedDestinationPath}" is already taken.`); 32 | } 33 | 34 | const destinationFilename = path.basename(verifiedDestinationPath); 35 | 36 | const newFileDetails = await updateFileDetails(verifiedSourcePath, { 37 | name: destinationFilename, 38 | directory: destinationDirectory, 39 | fullPath: verifiedDestinationPath, 40 | }); 41 | 42 | await removeFile(verifiedSourcePath); 43 | 44 | return newFileDetails as IFileEntry; 45 | }; 46 | -------------------------------------------------------------------------------- /lib/framework/parts/details-instance/details-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, details, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'details', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('details Function', () => { 13 | functionImportTest(details); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(details('wrone name')).rejects.toThrow('"wrone name" path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the file or directory does not exist', async () => { 20 | await expect(details('file.txt')).rejects.toThrow('"root/file.txt" file or directory does not exist.'); 21 | 22 | await expect(details('test/file.txt')).rejects.toThrow('"root/test/file.txt" file or directory does not exist.'); 23 | }); 24 | 25 | it('should return details about file', async () => { 26 | const createdFile = await writeFile('file.txt', 'test 2 content'); 27 | const createdFileDetails = await details(createdFile.fullPath); 28 | 29 | expect(createdFileDetails.type).toEqual('file'); 30 | expect(createdFileDetails.name).toEqual('file.txt'); 31 | expect(createdFileDetails.directory).toEqual('root'); 32 | expect(createdFileDetails.fullPath).toEqual('root/file.txt'); 33 | }); 34 | 35 | it('should return details about directory', async () => { 36 | const createdDirectory = await createDirectory('directory'); 37 | const createdDirectoryDetails = await details(createdDirectory.fullPath); 38 | 39 | expect(createdDirectoryDetails.type).toEqual('directory'); 40 | expect(createdDirectoryDetails.name).toEqual('directory'); 41 | expect(createdDirectoryDetails.directory).toEqual('root'); 42 | expect(createdDirectoryDetails.fullPath).toEqual('root/directory'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /lib/framework/parts/directory-details-instance/directory-details-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, directoryDetails, exists, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'directoryDetails', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('directoryDetails Function', () => { 13 | functionImportTest(directoryDetails); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(directoryDetails('test//test2 ')).rejects.toThrow('"test//test2 " path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the file does not exist', async () => { 20 | await expect(directoryDetails('file.txt')).rejects.toThrow('"root/file.txt" directory does not exist.'); 21 | 22 | await expect(directoryDetails('test/file.txt')).rejects.toThrow('"root/test/file.txt" directory does not exist.'); 23 | }); 24 | 25 | it('should throw type error when selected target is not a directory', async () => { 26 | await writeFile('file_as_directory', 'content'); 27 | await expect(exists('file_as_directory')).resolves.toBeTruthy(); 28 | 29 | await expect(directoryDetails('file_as_directory')).rejects.toThrow('"root/file_as_directory" is not a directory.'); 30 | }); 31 | 32 | it('should return details about directory', async () => { 33 | const createdDirectory = await createDirectory('directory'); 34 | const createdDirectoryDetails = await directoryDetails(createdDirectory.fullPath); 35 | 36 | expect(createdDirectoryDetails.isRoot).toBeFalsy(); 37 | expect(createdDirectoryDetails.directory).toEqual('root'); 38 | expect(createdDirectoryDetails.type).toEqual('directory'); 39 | expect(createdDirectoryDetails.name).toEqual('directory'); 40 | expect(createdDirectoryDetails.fullPath).toEqual('root/directory'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /lib/framework/parts/rename-file-instance/rename-file-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { exists, removeFile, renameFile, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'renameFile', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('renameFile Function', () => { 13 | functionImportTest(renameFile); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(renameFile('//invalid path', 'new_file.txt')).rejects.toThrow('"//invalid path" path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the user tries to rename the file that does not exist', async () => { 20 | await expect(renameFile('file.txt', 'root')).rejects.toThrow('"root/file.txt" file does not exist.'); 21 | }); 22 | 23 | it('should throw an error when target is not a file', async () => { 24 | await expect(renameFile('root', 'root')).rejects.toThrow('"root" is not a file.'); 25 | }); 26 | 27 | it('should throw an error when new filename is already taken', async () => { 28 | await writeFile('file.txt', 'content'); 29 | 30 | await expect(renameFile('file.txt', 'file.txt')).rejects.toThrow('"root/file.txt" is already taken.'); 31 | 32 | await removeFile('file.txt'); 33 | }); 34 | 35 | it('should return renamed file on success', async () => { 36 | const writtenFile = await writeFile('test_file.txt', 'content'); 37 | const renamedFile = await renameFile('test_file.txt', 'renamed_file.txt'); 38 | 39 | await expect(exists('test_file.txt')).resolves.toBeFalsy(); 40 | await expect(exists('renamed_file.txt')).resolves.toBeTruthy(); 41 | 42 | expect(writtenFile.data).toEqual(renamedFile.data); 43 | expect(writtenFile.type).toEqual(renamedFile.type); 44 | expect(writtenFile.createdAt).toEqual(renamedFile.createdAt); 45 | expect(writtenFile.directory).toEqual(renamedFile.directory); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /lib/framework/__snapshots__/create-fs.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createFs Function fs object creation should throw an error when passed configuration is invalid 1`] = ` 4 | "Props passed to createFS function are invalid: 5 | [{"path":["databaseName"],"property":"instance.databaseName","message":"does not meet maximum length of 50","schema":{"minLength":4,"maxLength":50,"type":"string","pattern":"^[a-zA-Z0-9_.-]*$"},"instance":"too long database name as example of databaseName parameter","name":"maxLength","argument":50,"stack":"instance.databaseName does not meet maximum length of 50"},{"path":["databaseName"],"property":"instance.databaseName","message":"does not match pattern \\"^[a-zA-Z0-9_.-]*$\\"","schema":{"minLength":4,"maxLength":50,"type":"string","pattern":"^[a-zA-Z0-9_.-]*$"},"instance":"too long database name as example of databaseName parameter","name":"pattern","argument":"^[a-zA-Z0-9_.-]*$","stack":"instance.databaseName does not match pattern \\"^[a-zA-Z0-9_.-]*$\\""},{"path":["objectStoreName"],"property":"instance.objectStoreName","message":"does not match pattern \\"^[a-zA-Z0-9_.-]*$\\"","schema":{"minLength":1,"maxLength":20,"type":"string","pattern":"^[a-zA-Z0-9_.-]*$"},"instance":" starts with space","name":"pattern","argument":"^[a-zA-Z0-9_.-]*$","stack":"instance.objectStoreName does not match pattern \\"^[a-zA-Z0-9_.-]*$\\""},{"path":["rootDirectoryName"],"property":"instance.rootDirectoryName","message":"does not meet minimum length of 1","schema":{"minLength":1,"maxLength":20,"type":"string","pattern":"^[a-zA-Z0-9_.-]*$"},"instance":"","name":"minLength","argument":1,"stack":"instance.rootDirectoryName does not meet minimum length of 1"},{"path":["databaseVersion"],"property":"instance.databaseVersion","message":"is not of a type(s) integer","schema":{"minimum":1,"maximum":100,"type":"integer"},"instance":1.5,"name":"type","argument":["integer"],"stack":"instance.databaseVersion is not of a type(s) integer"}]" 6 | `; 7 | 8 | exports[`createFs Function should throw an error when the browser does not support indexedDB 1`] = `"Your browser does not support indexedDB."`; 9 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const babel = require('@rollup/plugin-babel'); 2 | const { terser } = require('rollup-plugin-terser'); 3 | const commonjs = require('@rollup/plugin-commonjs'); 4 | const typescript = require('rollup-plugin-typescript2'); 5 | const polyfills = require('rollup-plugin-polyfill-node'); 6 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 7 | 8 | const input = 'lib/index.ts'; 9 | 10 | module.exports = [ 11 | // ES Modules 12 | { 13 | input, 14 | output: { 15 | format: 'es', 16 | file: 'dist/index.es.js', 17 | exports: 'named', 18 | }, 19 | plugins: [ 20 | polyfills(), 21 | typescript({ 22 | tsconfig: 'tsconfig.build.json', 23 | typescript: require('ttypescript'), 24 | tsconfigDefaults: { 25 | compilerOptions: { 26 | plugins: [ 27 | { transform: 'typescript-transform-paths' }, 28 | { transform: 'typescript-transform-paths', afterDeclarations: true }, 29 | ], 30 | }, 31 | }, 32 | }), 33 | babel({ 34 | extensions: ['.ts'], 35 | babelHelpers: 'bundled', 36 | }), 37 | nodeResolve({ preferBuiltins: false }), 38 | commonjs(), 39 | ], 40 | }, 41 | // UMD 42 | { 43 | input, 44 | output: { 45 | format: 'umd', 46 | file: 'dist/index.umd.min.js', 47 | name: 'indexeddb-fs', 48 | exports: 'named', 49 | indent: false, 50 | }, 51 | plugins: [ 52 | polyfills(), 53 | typescript({ 54 | tsconfig: 'tsconfig.build.json', 55 | typescript: require('ttypescript'), 56 | tsconfigDefaults: { 57 | compilerOptions: { 58 | plugins: [ 59 | { transform: 'typescript-transform-paths' }, 60 | { transform: 'typescript-transform-paths', afterDeclarations: true }, 61 | ], 62 | }, 63 | }, 64 | }), 65 | babel({ 66 | extensions: ['.ts'], 67 | babelHelpers: 'bundled', 68 | exclude: 'node_modules/**', 69 | }), 70 | terser(), 71 | nodeResolve({ preferBuiltins: false }), 72 | commonjs(), 73 | ], 74 | }, 75 | ]; 76 | -------------------------------------------------------------------------------- /lib/utils/functions/has-root-directory-prefix/has-root-directory-prefix.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest } from '@utils'; 2 | 3 | import { hasRootDirectoryPrefix } from './has-root-directory-prefix.function'; 4 | 5 | describe('hasRootDirectoryPrefix Function', () => { 6 | functionImportTest(hasRootDirectoryPrefix); 7 | 8 | it('should return false when rootDirectoryName value is not a string', () => { 9 | // @ts-expect-error 10 | expect(hasRootDirectoryPrefix({})).toBeFalsy(); 11 | 12 | // @ts-expect-error 13 | expect(hasRootDirectoryPrefix(null)).toBeFalsy(); 14 | 15 | // @ts-expect-error 16 | expect(hasRootDirectoryPrefix(-500)).toBeFalsy(); 17 | 18 | // @ts-expect-error 19 | expect(hasRootDirectoryPrefix(undefined)).toBeFalsy(); 20 | 21 | // @ts-expect-error 22 | expect(hasRootDirectoryPrefix([1, 2, 3])).toBeFalsy(); 23 | 24 | // @ts-expect-error 25 | expect(hasRootDirectoryPrefix(new Date())).toBeFalsy(); 26 | }); 27 | 28 | it('should return false when fullPath value is not a string', () => { 29 | // @ts-expect-error 30 | expect(hasRootDirectoryPrefix('test', {})).toBeFalsy(); 31 | 32 | // @ts-expect-error 33 | expect(hasRootDirectoryPrefix('test', null)).toBeFalsy(); 34 | 35 | // @ts-expect-error 36 | expect(hasRootDirectoryPrefix('test', -500)).toBeFalsy(); 37 | 38 | // @ts-expect-error 39 | expect(hasRootDirectoryPrefix('test', undefined)).toBeFalsy(); 40 | 41 | // @ts-expect-error 42 | expect(hasRootDirectoryPrefix('test', [1, 2, 3])).toBeFalsy(); 43 | 44 | // @ts-expect-error 45 | expect(hasRootDirectoryPrefix('test', new Date())).toBeFalsy(); 46 | }); 47 | 48 | it('should return false when fullPath does not starts with rootDirectoryName value', () => { 49 | expect(hasRootDirectoryPrefix('/example', '')).toBeFalsy(); 50 | expect(hasRootDirectoryPrefix('full_path/example', '')).toBeFalsy(); 51 | expect(hasRootDirectoryPrefix('full_path/example', 'full')).toBeFalsy(); 52 | expect(hasRootDirectoryPrefix('full_path/example', 'full_pat')).toBeFalsy(); 53 | }); 54 | 55 | it('should return true when fullPath starts with rootDirectoryName value', () => { 56 | expect(hasRootDirectoryPrefix('full_path/example', 'full_path')).toBeTruthy(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /lib/framework/parts/remove-directory-instance/remove-directory-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, exists, readDirectory, removeDirectory, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'removeDirectory', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('removeDirectory Function', () => { 13 | functionImportTest(removeDirectory); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(removeDirectory('//path')).rejects.toThrow('"//path" path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when passed path does not exist', async () => { 20 | await expect(removeDirectory('path')).rejects.toThrow('"root/path" directory does not exist.'); 21 | }); 22 | 23 | it('should throw an error when passed path is not a directory', async () => { 24 | await writeFile('file_as_directory', 'content'); 25 | 26 | await expect(removeDirectory('file_as_directory')).rejects.toThrow('"file_as_directory" is not a directory.'); 27 | }); 28 | 29 | it('should remove files and directories of passed fullPath', async () => { 30 | await createDirectory('test_directory'); 31 | await writeFile('test_directory/file.txt', 'content'); 32 | await createDirectory('test_directory/foo'); 33 | await createDirectory('test_directory/folder'); 34 | await createDirectory('test_directory/folder/foo'); 35 | await createDirectory('test_directory/folder/foo/foo2'); 36 | await createDirectory('test_directory/folder/foo/foo2/foo5'); 37 | await createDirectory('test_directory/folder/foo/foo2/foo3'); 38 | await createDirectory('test_directory/folder/foo/foo2/file.txt'); 39 | await createDirectory('test_directory/folder/foo/foo2/foo3/foo4'); 40 | 41 | await removeDirectory('test_directory/folder/foo/foo2'); 42 | await expect(exists('test_directory/folder/foo/foo2')).resolves.toBeFalsy(); 43 | 44 | const { directories, files } = await readDirectory('test_directory'); 45 | 46 | expect([...files, ...directories]).toHaveLength(3); 47 | 48 | await removeDirectory('test_directory'); 49 | await expect(exists('test_directory')).resolves.toBeFalsy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .pnpm-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | .env.production 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | .parcel-cache 79 | 80 | # Next.js build output 81 | .next 82 | out 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | .yarn/cache 114 | .yarn/unplugged 115 | .yarn/build-state.yml 116 | .yarn/install-state.gz 117 | .pnp.* 118 | 119 | temp 120 | .DS_Store 121 | index.html 122 | -------------------------------------------------------------------------------- /lib/framework/parts/read-directory-instance/read-directory-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, exists, readDirectory, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'readDirectory', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('readDirectory Function', () => { 13 | functionImportTest(readDirectory); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(readDirectory('//path')).rejects.toThrow('"//path" path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when passed path does not exist', async () => { 20 | await expect(readDirectory('path')).rejects.toThrow('"root/path" directory does not exist.'); 21 | }); 22 | 23 | it('should return empty array when directory is empty', async () => { 24 | await createDirectory('test_directory'); 25 | 26 | await expect(readDirectory('test_directory')).resolves.toEqual({ 27 | files: [], 28 | filesCount: 0, 29 | isEmpty: true, 30 | directories: [], 31 | directoriesCount: 0, 32 | }); 33 | }); 34 | 35 | it('should throw type error when selected target is not a directory', async () => { 36 | await writeFile('test_file.txt', 'content'); 37 | await expect(exists('test_file.txt')).resolves.toBeTruthy(); 38 | 39 | await expect(readDirectory('test_file.txt')).rejects.toThrow('"root/test_file.txt" is not a directory.'); 40 | }); 41 | 42 | it('should return array of found objects', async () => { 43 | await createDirectory('test_directory'); 44 | await writeFile('test_directory/file.txt', 'content'); 45 | await createDirectory('test_directory/folder'); 46 | 47 | const { directories, directoriesCount, files, filesCount } = await readDirectory('test_directory'); 48 | 49 | expect(filesCount).toEqual(1); 50 | expect(directoriesCount).toEqual(1); 51 | 52 | expect(files[0].type).toEqual('file'); 53 | expect(files[0].name).toEqual('file.txt'); 54 | expect(files[0].directory).toEqual('root/test_directory'); 55 | expect(files[0].fullPath).toEqual('root/test_directory/file.txt'); 56 | 57 | expect(directories[0].name).toEqual('folder'); 58 | expect(directories[0].type).toEqual('directory'); 59 | expect(directories[0].directory).toEqual('root/test_directory'); 60 | expect(directories[0].fullPath).toEqual('root/test_directory/folder'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /lib/framework/parts/update-file-details-instance/update-file-details-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { getDatabaseCrud } from '@database'; 4 | 5 | import { functionImportTest } from '@utils'; 6 | 7 | import { updateFileDetailsInstance } from './update-file-details-instance.function'; 8 | 9 | const rootDirectoryName = 'root'; 10 | 11 | const { putRecord } = getDatabaseCrud({ 12 | databaseVersion: 1, 13 | objectStoreName: 'files', 14 | databaseName: 'indexeddb-fs', 15 | }); 16 | 17 | const { createDirectory, fileDetails, isDirectory, removeDirectory, removeFile, writeFile } = createFs(); 18 | 19 | const updateFileDetails = updateFileDetailsInstance({ 20 | putRecord, 21 | fileDetails, 22 | isDirectory, 23 | rootDirectoryName, 24 | }); 25 | 26 | describe('updateFileDetails Function', () => { 27 | functionImportTest(updateFileDetails); 28 | 29 | it('should throw an error when the directory where the file is to be placed does not exist', async () => { 30 | await writeFile('file.txt', 'content'); 31 | 32 | await expect(updateFileDetails('file.txt/test', { name: 'file.txt' })).rejects.toThrow( 33 | '"root/file.txt" is not a directory.', 34 | ); 35 | 36 | await removeFile('file.txt'); 37 | }); 38 | 39 | it('should throw an error when a user tries to edit a root directory', async () => { 40 | await expect(updateFileDetails(rootDirectoryName, { name: 'file.txt' })).rejects.toThrow( 41 | 'Root directory: "root" cannot be updated.', 42 | ); 43 | }); 44 | 45 | it('should throw an error when a user tries to edit a directory', async () => { 46 | await createDirectory('files'); 47 | 48 | await expect(updateFileDetails('root/files', { name: 'file.txt' })).rejects.toThrow( 49 | '"root/files" you cannot update a directory.', 50 | ); 51 | 52 | await removeDirectory('files'); 53 | }); 54 | 55 | it('should update details about existing file', async () => { 56 | await createDirectory('files'); 57 | const createdFile = await writeFile('files/file.txt', 'file content'); 58 | 59 | expect(createdFile.data).toEqual('file content'); 60 | 61 | const updatedFile = await updateFileDetails('files/file.txt', { data: 'update' }); 62 | 63 | expect(updatedFile.data).toEqual('update'); 64 | 65 | expect(createdFile.type).toEqual(updatedFile.type); 66 | expect(createdFile.name).toEqual(updatedFile.name); 67 | expect(createdFile.fullPath).toEqual(updatedFile.fullPath); 68 | expect(createdFile.createdAt).toEqual(updatedFile.createdAt); 69 | expect(createdFile.directory).toEqual(updatedFile.directory); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /lib/framework/parts/create-directory-instance/create-directory-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | rootDirectoryName: 'root', 8 | databaseName: 'createDirectory', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('createDirectory Function', () => { 13 | functionImportTest(createDirectory); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(createDirectory('test//test2 ')).rejects.toThrow('"test//test2 " path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the user tries to create a root directory', async () => { 20 | await expect(createDirectory('root')).rejects.toThrow('Root directory: "root" already exist.'); 21 | }); 22 | 23 | it('should throw an error when user wants to create a folder in another one that does not exist', async () => { 24 | await expect(createDirectory('test/test2')).rejects.toThrow('"root/test" directory does not exist.'); 25 | }); 26 | 27 | it('should throw an error when the user tries to create a directory in the target of type file', async () => { 28 | await writeFile('target_of_type_file', 'content of file'); 29 | 30 | await expect(createDirectory('target_of_type_file/type')).rejects.toThrow( 31 | '"root/target_of_type_file" is not a directory.', 32 | ); 33 | }); 34 | 35 | it('should create a directory in root directory', async () => { 36 | const result = await createDirectory('test1'); 37 | 38 | expect(result.name).toEqual('test1'); 39 | expect(result.type).toEqual('directory'); 40 | expect(result.directory).toEqual('root'); 41 | expect(result.fullPath).toEqual('root/test1'); 42 | }); 43 | 44 | it('should create directory in other existing directory', async () => { 45 | const resultForTest1 = await createDirectory('test2'); 46 | 47 | expect(resultForTest1.isRoot).toBeFalsy(); 48 | expect(resultForTest1.name).toEqual('test2'); 49 | expect(resultForTest1.type).toEqual('directory'); 50 | expect(resultForTest1.directory).toEqual('root'); 51 | expect(resultForTest1.fullPath).toEqual('root/test2'); 52 | 53 | const resultForTest2 = await createDirectory('test2/test3'); 54 | 55 | expect(resultForTest2.isRoot).toBeFalsy(); 56 | expect(resultForTest2.name).toEqual('test3'); 57 | expect(resultForTest2.type).toEqual('directory'); 58 | expect(resultForTest2.directory).toEqual('root/test2'); 59 | expect(resultForTest2.fullPath).toEqual('root/test2/test3'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/framework/parts/move-file-instance/move-file-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, exists, moveFile, readFile, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'moveFile', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('moveFile Function', () => { 13 | functionImportTest(moveFile); 14 | 15 | describe('paths validation', () => { 16 | it('should throw an error when sourcePath parameter is invalid', async () => { 17 | await expect(moveFile('source path', 'legit')).rejects.toThrow('"source path" path is invalid.'); 18 | }); 19 | 20 | it('should throw an error when destinationPath parameter is invalid', async () => { 21 | await expect(moveFile('source_path', 'destination path')).rejects.toThrow('"destination path" path is invalid.'); 22 | }); 23 | }); 24 | 25 | describe('sourcePath access validation', () => { 26 | it('should throw an error when does not exist', async () => { 27 | await expect(moveFile('file.txt', 'test.txt')).rejects.toThrow('"root/file.txt" file does not exist.'); 28 | }); 29 | 30 | it('should throw an error when it is not a file', async () => { 31 | await createDirectory('directory_as_a_file_1'); 32 | 33 | await expect(moveFile('directory_as_a_file_1', 'test.txt')).rejects.toThrow( 34 | '"root/directory_as_a_file_1" source is not a file.', 35 | ); 36 | }); 37 | }); 38 | 39 | describe('destinationPath access validation', () => { 40 | it('should throw an error when the destination directory does not exist', async () => { 41 | await writeFile('file.txt', 'file content'); 42 | 43 | await expect(moveFile('file.txt', 'test/test.txt')).rejects.toThrow('"root/test" directory does not exist.'); 44 | }); 45 | 46 | it('should throw an error when the destination point is already taken', async () => { 47 | await writeFile('file.txt', 'file content'); 48 | 49 | await expect(moveFile('file.txt', 'directory_as_a_file_1')).rejects.toThrow( 50 | '"root/directory_as_a_file_1" is already taken.', 51 | ); 52 | }); 53 | }); 54 | 55 | it('should move a file to a brand new destination point', async () => { 56 | await createDirectory('moved_files'); 57 | await writeFile('root_file.txt', 'root file content'); 58 | 59 | await moveFile('root_file.txt', 'moved_files/file.txt'); 60 | 61 | await expect(exists('root_file.txt')).resolves.toBeFalsy(); 62 | await expect(readFile('moved_files/file.txt')).resolves.toEqual('root file content'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /lib/utils/functions/is-valid-path/is-valid-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest, isValidPath } from '@utils'; 2 | 3 | describe('isValidPath Function', () => { 4 | functionImportTest(isValidPath); 5 | 6 | it('should return false when passed parameter is not a string value', () => { 7 | // @ts-expect-error 8 | expect(isValidPath({})).toBeFalsy(); 9 | 10 | // @ts-expect-error 11 | expect(isValidPath(null)).toBeFalsy(); 12 | 13 | // @ts-expect-error 14 | expect(isValidPath(-500)).toBeFalsy(); 15 | 16 | // @ts-expect-error 17 | expect(isValidPath(undefined)).toBeFalsy(); 18 | 19 | // @ts-expect-error 20 | expect(isValidPath([1, 2, 3])).toBeFalsy(); 21 | 22 | // @ts-expect-error 23 | expect(isValidPath(new Date())).toBeFalsy(); 24 | }); 25 | 26 | it('should return false when the provided path is an invalid value', () => { 27 | expect(isValidPath('')).toBeFalsy(); 28 | expect(isValidPath('dir1')).toBeFalsy(); 29 | expect(isValidPath('dir1/')).toBeFalsy(); 30 | expect(isValidPath('/dir1')).toBeFalsy(); 31 | expect(isValidPath('bucket.name')).toBeFalsy(); 32 | expect(isValidPath('/bucket.name')).toBeFalsy(); 33 | expect(isValidPath('bucket.name/')).toBeFalsy(); 34 | expect(isValidPath('dir1/dir2/dir3/')).toBeFalsy(); 35 | expect(isValidPath('C:dir1\blah.txt')).toBeFalsy(); 36 | expect(isValidPath('/bucket.name/dir')).toBeFalsy(); 37 | expect(isValidPath('/dir1/dir2/dir3/')).toBeFalsy(); 38 | expect(isValidPath('/dir1/filename.ext')).toBeFalsy(); 39 | expect(isValidPath('bucket.name/dir/filename/')).toBeFalsy(); 40 | expect(isValidPath('/bucket.name/dir/filename/')).toBeFalsy(); 41 | expect(isValidPath('.bucket.name/dir/filename.ext')).toBeFalsy(); 42 | expect(isValidPath('bucket.name./dir/filename.ext')).toBeFalsy(); 43 | expect(isValidPath('bucket..name/dir/filename.ext')).toBeFalsy(); 44 | expect(isValidPath('bucket.name//dir//filename.ext')).toBeFalsy(); 45 | expect(isValidPath('/bucket.name//dir//filename.ext')).toBeFalsy(); 46 | 47 | expect(isValidPath('bucket.name/dir/filename with spaces!.and_multiple_full_stops.ext')).toBeFalsy(); 48 | 49 | expect( 50 | isValidPath( 51 | 'bucket.name/dir/filename with all valid path characters, but some that we`re not allowing !£$%^&()-=[]`#`#,.ext', 52 | ), 53 | ).toBeFalsy(); 54 | }); 55 | 56 | it('should return true when the provided path is a valid value', () => { 57 | expect(isValidPath('C:/dir1/blah.txt')).toBeTruthy(); 58 | expect(isValidPath('dir1/filename.ext')).toBeTruthy(); 59 | expect(isValidPath('dir1/dir2/filename.ext')).toBeTruthy(); 60 | expect(isValidPath('bucketname/filename.ext')).toBeTruthy(); 61 | expect(isValidPath('bucket.name/filename.ext')).toBeTruthy(); 62 | expect(isValidPath('bucket.name/dir1/filename.ext')).toBeTruthy(); 63 | expect(isValidPath('bucket.name/dir2/filename.ext')).toBeTruthy(); 64 | expect(isValidPath('valid.bucket.name._-0123456789/filename.ext')).toBeTruthy(); 65 | expect(isValidPath('bucket.name/2015-01-17/15.00_description.ext')).toBeTruthy(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /lib/framework/parts/write-file-instance/write-file-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { createDirectory, removeDirectory, removeFile, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'writeFile', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('writeFile Function', () => { 13 | functionImportTest(writeFile); 14 | 15 | it('should throw an error when fullPath parameter is invalid', async () => { 16 | await expect(writeFile('test//test2 ', 'content')).rejects.toThrow('"test//test2 " path is invalid.'); 17 | }); 18 | 19 | it('should throw an error when the user tries to create a root directory as a file', async () => { 20 | await expect(writeFile('root', 'root')).rejects.toThrow('Root directory: "root" cannot be a file.'); 21 | }); 22 | 23 | it('should throw an error when user wants to create a file in a folder that does not exist', async () => { 24 | await expect(writeFile('test3/test2/test/file.txt', 'content')).rejects.toThrow( 25 | '"root/test3/test2/test" directory does not exist.', 26 | ); 27 | }); 28 | 29 | it('should create file in root directory', async () => { 30 | const result = await writeFile('file1.txt', 'test content'); 31 | 32 | expect(result.type).toEqual('file'); 33 | expect(result.name).toEqual('file1.txt'); 34 | expect(result.directory).toEqual('root'); 35 | expect(result.data).toEqual('test content'); 36 | expect(result.fullPath).toEqual('root/file1.txt'); 37 | 38 | await removeFile('file1.txt'); 39 | }); 40 | 41 | it('should create a file in other existing directory', async () => { 42 | await createDirectory('test2'); 43 | const result = await writeFile('test2/file.txt', 'content'); 44 | 45 | expect(result.type).toEqual('file'); 46 | expect(result.data).toEqual('content'); 47 | expect(result.name).toEqual('file.txt'); 48 | expect(result.directory).toEqual('root/test2'); 49 | expect(result.fullPath).toEqual('root/test2/file.txt'); 50 | 51 | await removeDirectory('test2'); 52 | }); 53 | 54 | it('should create a file with object data', async () => { 55 | await createDirectory('test2'); 56 | const result = await writeFile('test2/file3.txt', { test: 'object' }); 57 | 58 | expect(result.type).toEqual('file'); 59 | expect(result.name).toEqual('file3.txt'); 60 | expect(result.directory).toEqual('root/test2'); 61 | expect(result.data).toEqual({ test: 'object' }); 62 | expect(result.fullPath).toEqual('root/test2/file3.txt'); 63 | 64 | await removeDirectory('test2'); 65 | }); 66 | 67 | it('should throw an error when a user tries to create a file with the same name as the directory', async () => { 68 | await createDirectory('example_of_directory'); 69 | 70 | await expect(writeFile('example_of_directory', { test: 'object' })).rejects.toThrow( 71 | '"root/example_of_directory" you cannot create a file with the same name as the directory.', 72 | ); 73 | 74 | await removeDirectory('example_of_directory'); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /lib/framework/parts/copy-file-instance/copy-file-instance.spec.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '@framework/create-fs.function'; 2 | 3 | import { functionImportTest } from '@utils'; 4 | 5 | const { copyFile, createDirectory, readFile, removeDirectory, removeFile, writeFile } = createFs({ 6 | databaseVersion: 1, 7 | databaseName: 'copyFile', 8 | rootDirectoryName: 'root', 9 | objectStoreName: 'objectStoreName', 10 | }); 11 | 12 | describe('copyFile Function', () => { 13 | functionImportTest(copyFile); 14 | 15 | describe('paths validation', () => { 16 | it('should throw an error when sourcePath parameter is invalid', async () => { 17 | await expect(copyFile('source path', 'legit')).rejects.toThrow('"source path" path is invalid.'); 18 | }); 19 | 20 | it('should throw an error when destinationPath parameter is invalid', async () => { 21 | await expect(copyFile('source_path', 'destination path')).rejects.toThrow('"destination path" path is invalid.'); 22 | }); 23 | }); 24 | 25 | describe('sourcePath access validation', () => { 26 | it('should throw an error when does not exist', async () => { 27 | await expect(copyFile('file.txt', 'test.txt')).rejects.toThrow('"root/file.txt" file does not exist.'); 28 | }); 29 | 30 | it('should throw an error when it is not a file', async () => { 31 | await createDirectory('directory_as_a_file_1'); 32 | 33 | await expect(copyFile('directory_as_a_file_1', 'test.txt')).rejects.toThrow( 34 | '"root/directory_as_a_file_1" source is not a file.', 35 | ); 36 | 37 | await removeDirectory('directory_as_a_file_1'); 38 | }); 39 | }); 40 | 41 | describe('destinationPath access validation', () => { 42 | it('should throw an error when the destination directory does not exist', async () => { 43 | await writeFile('file.txt', 'file content'); 44 | 45 | await expect(copyFile('file.txt', 'test/test.txt')).rejects.toThrow('"root/test" directory does not exist.'); 46 | 47 | await removeFile('file.txt'); 48 | }); 49 | 50 | it('should throw an error when the destination point is already taken', async () => { 51 | await writeFile('file.txt', 'content'); 52 | 53 | await expect(copyFile('file.txt', 'file.txt')).rejects.toThrow('"root/file.txt" is already taken.'); 54 | 55 | await removeFile('file.txt'); 56 | }); 57 | 58 | it('should throw an error when the destination directory is file', async () => { 59 | await writeFile('file2.txt', 'file content'); 60 | 61 | await expect(copyFile('file2.txt', 'file2.txt/content')).rejects.toThrow( 62 | '"root/file2.txt" destination directory does not exist.', 63 | ); 64 | 65 | await removeFile('file2.txt'); 66 | }); 67 | 68 | it('should throw an error when the destination path is file', async () => { 69 | await createDirectory('directory_as_a_file_1'); 70 | await writeFile('file.txt', 'file content'); 71 | 72 | await expect(copyFile('file.txt', 'directory_as_a_file_1')).rejects.toThrow( 73 | '"root/directory_as_a_file_1" is already taken.', 74 | ); 75 | 76 | await removeFile('file.txt'); 77 | await removeDirectory('directory_as_a_file_1'); 78 | }); 79 | }); 80 | 81 | it('should copy a file to a brand new destination point', async () => { 82 | await createDirectory('copied_files'); 83 | await writeFile('root_file.txt', 'root file content'); 84 | 85 | await copyFile('root_file.txt', 'copied_files/file.txt'); 86 | 87 | await expect(readFile('root_file.txt')).resolves.toEqual('root file content'); 88 | await expect(readFile('copied_files/file.txt')).resolves.toEqual('root file content'); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /lib/utils/functions/validate-create-fs-props/__snapshots__/validate-create-fs-props.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validateCreateFsProps Function should require all object fields 1`] = ` 4 | "Props passed to createFS function are invalid: 5 | [{"path":[],"property":"instance","message":"requires property \\"databaseName\\"","schema":"/CreateFsPropsSchema","instance":{},"name":"required","argument":"databaseName","stack":"instance requires property \\"databaseName\\""},{"path":[],"property":"instance","message":"requires property \\"databaseVersion\\"","schema":"/CreateFsPropsSchema","instance":{},"name":"required","argument":"databaseVersion","stack":"instance requires property \\"databaseVersion\\""},{"path":[],"property":"instance","message":"requires property \\"objectStoreName\\"","schema":"/CreateFsPropsSchema","instance":{},"name":"required","argument":"objectStoreName","stack":"instance requires property \\"objectStoreName\\""},{"path":[],"property":"instance","message":"requires property \\"rootDirectoryName\\"","schema":"/CreateFsPropsSchema","instance":{},"name":"required","argument":"rootDirectoryName","stack":"instance requires property \\"rootDirectoryName\\""}]" 6 | `; 7 | 8 | exports[`validateCreateFsProps Function should throw an error when passed value is not an object 1`] = ` 9 | "Props passed to createFS function are invalid: 10 | [{"path":[],"property":"instance","message":"is not of a type(s) object","schema":"/CreateFsPropsSchema","instance":null,"name":"type","argument":["object"],"stack":"instance is not of a type(s) object"}]" 11 | `; 12 | 13 | exports[`validateCreateFsProps Function should validate passed value 1`] = ` 14 | "Props passed to createFS function are invalid: 15 | [{"path":["databaseName"],"property":"instance.databaseName","message":"does not meet minimum length of 4","schema":{"minLength":4,"maxLength":50,"type":"string","pattern":"^[a-zA-Z0-9_.-]*$"},"instance":"d","name":"minLength","argument":4,"stack":"instance.databaseName does not meet minimum length of 4"},{"path":["objectStoreName"],"property":"instance.objectStoreName","message":"does not meet maximum length of 20","schema":{"minLength":1,"maxLength":20,"type":"string","pattern":"^[a-zA-Z0-9_.-]*$"},"instance":"_contain_string_contain_string_contain_string_contain_string","name":"maxLength","argument":20,"stack":"instance.objectStoreName does not meet maximum length of 20"},{"path":["rootDirectoryName"],"property":"instance.rootDirectoryName","message":"does not match pattern \\"^[a-zA-Z0-9_.-]*$\\"","schema":{"minLength":1,"maxLength":20,"type":"string","pattern":"^[a-zA-Z0-9_.-]*$"},"instance":" start_with_string","name":"pattern","argument":"^[a-zA-Z0-9_.-]*$","stack":"instance.rootDirectoryName does not match pattern \\"^[a-zA-Z0-9_.-]*$\\""},{"path":["databaseVersion"],"property":"instance.databaseVersion","message":"is not of a type(s) integer","schema":{"minimum":1,"maximum":100,"type":"integer"},"instance":1.1,"name":"type","argument":["integer"],"stack":"instance.databaseVersion is not of a type(s) integer"}]" 16 | `; 17 | 18 | exports[`validateCreateFsProps Function snapshot test should match snapshot 1`] = ` 19 | { 20 | "id": "/CreateFsPropsSchema", 21 | "properties": { 22 | "databaseName": { 23 | "maxLength": 50, 24 | "minLength": 4, 25 | "pattern": "^[a-zA-Z0-9_.-]*$", 26 | "type": "string", 27 | }, 28 | "databaseVersion": { 29 | "maximum": 100, 30 | "minimum": 1, 31 | "type": "integer", 32 | }, 33 | "objectStoreName": { 34 | "maxLength": 20, 35 | "minLength": 1, 36 | "pattern": "^[a-zA-Z0-9_.-]*$", 37 | "type": "string", 38 | }, 39 | "rootDirectoryName": { 40 | "maxLength": 20, 41 | "minLength": 1, 42 | "pattern": "^[a-zA-Z0-9_.-]*$", 43 | "type": "string", 44 | }, 45 | }, 46 | "required": [ 47 | "databaseName", 48 | "databaseVersion", 49 | "objectStoreName", 50 | "rootDirectoryName", 51 | ], 52 | "type": "object", 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-typescript/base', 'plugin:unicorn/recommended', 'plugin:typescript-sort-keys/recommended'], 3 | env: { 4 | node: true, 5 | browser: true, 6 | }, 7 | parserOptions: { 8 | project: ['./tsconfig.json'], 9 | }, 10 | plugins: ['folders', 'sort-destructure-keys', 'unused-imports'], 11 | overrides: [ 12 | { 13 | files: ['*.js', '*.ts', '*.tsx'], 14 | rules: { 15 | 'no-console': 'off', 16 | 'linebreak-style': 0, 17 | 'no-continue': 'off', 18 | 'consistent-return': 'off', 19 | 'prefer-arrow-callback': 2, 20 | 'operator-linebreak': 'off', 21 | 'no-confusing-arrow': 'off', 22 | 'no-underscore-dangle': 'off', 23 | 'object-curly-newline': 'off', 24 | 'no-restricted-syntax': 'off', 25 | 'no-prototype-builtins': 'off', 26 | 'class-methods-use-this': 'off', 27 | 'function-paren-newline': 'off', 28 | 'newline-per-chained-call': 'off', 29 | 'padding-line-between-statements': [ 30 | 'error', 31 | { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, 32 | { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, 33 | { blankLine: 'always', prev: 'directive', next: '*' }, 34 | { blankLine: 'any', prev: 'directive', next: 'directive' }, 35 | { blankLine: 'always', prev: 'if', next: '*' }, 36 | { blankLine: 'always', prev: '*', next: 'return' }, 37 | { blankLine: 'always', prev: 'function', next: 'function' }, 38 | ], 39 | 'implicit-arrow-linebreak': 'off', 40 | 'import/order': 'off', 41 | 'import/no-cycle': 'off', 42 | 'import/extensions': 'off', 43 | 'import/prefer-default-export': 'off', 44 | 'import/no-extraneous-dependencies': 'off', 45 | 'unicorn/no-null': 'off', 46 | 'unicorn/prefer-module': 'off', 47 | 'unicorn/no-array-reduce': 'off', 48 | 'unicorn/prefer-regexp-test': 'off', 49 | 'unicorn/no-useless-undefined': 'off', 50 | 'unicorn/prefer-node-protocol': 'off', 51 | 'unicorn/prefer-query-selector': 'off', 52 | 'unicorn/prevent-abbreviations': 'off', 53 | 'unicorn/prefer-array-index-of': 'off', 54 | 'unicorn/prefer-add-event-listener': 'off', 55 | 'unicorn/no-array-callback-reference': 'off', 56 | 'unicorn/no-object-as-default-parameter': 'off', 57 | 'unicorn/filename-case': 'off', 58 | 'unicorn/filename-case': [ 59 | 'error', 60 | { 61 | case: 'kebabCase', 62 | }, 63 | ], 64 | '@typescript-eslint/indent': 'off', 65 | '@typescript-eslint/no-shadow': 'off', 66 | '@typescript-eslint/comma-dangle': 'off', 67 | '@typescript-eslint/no-explicit-any': 'error', 68 | '@typescript-eslint/padding-line-between-statements': [ 69 | 'error', 70 | { 71 | blankLine: 'always', 72 | prev: ['interface', 'type'], 73 | next: '*', 74 | }, 75 | ], 76 | '@typescript-eslint/naming-convention': [ 77 | 'error', 78 | { 79 | selector: 'interface', 80 | format: ['PascalCase'], 81 | custom: { 82 | regex: '^I[A-Z]', 83 | match: true, 84 | }, 85 | }, 86 | { 87 | selector: 'enum', 88 | format: ['PascalCase'], 89 | custom: { 90 | regex: '^E[A-Z]', 91 | match: true, 92 | }, 93 | }, 94 | ], 95 | 'sort-destructure-keys/sort-destructure-keys': [2, { caseSensitive: false }], 96 | 'folders/match-regex': [2, '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', `${process.cwd()}/`], 97 | 'unused-imports/no-unused-imports': 'error', 98 | }, 99 | }, 100 | ], 101 | }; 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "indexeddb-fs", 3 | "version": "2.1.5", 4 | "description": "An 'fs' kind of library dedicated to the browser", 5 | "license": "MIT", 6 | "author": "Paweł Wojtasiński", 7 | "homepage": "https://github.com/playerony/indexeddb-fs#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/playerony/indexeddb-fs.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/playerony/indexeddb-fs/issues" 14 | }, 15 | "main": "dist/index.es.js", 16 | "module": "dist/index.es.js", 17 | "files": [ 18 | "/dist" 19 | ], 20 | "scripts": { 21 | "build": "yarn build:clean && yarn build:dist", 22 | "build:clean": "rimraf dist", 23 | "build:dist": "rollup -c", 24 | "check": "yarn lint && yarn typecheck && yarn test", 25 | "check:all": "concurrently \"yarn lint\" \"yarn typecheck\" \"yarn format:check\" \"yarn test\" \"yarn build\"", 26 | "commitlint": "commitlint -- --from=main", 27 | "format": "yarn prettier --write", 28 | "format:check": "yarn prettier --check", 29 | "gcb": "bash ./scripts/git-create-branch.sh", 30 | "gcdwp": "bash ./scripts/git-checkout-develop-with-pull.sh", 31 | "gpc": "bash ./scripts/git-push-changes.sh", 32 | "grlc": "bash ./scripts/git-reset-local-changes.sh", 33 | "lint": "eslint \"./lib/**/*\"", 34 | "lint:fix": "yarn lint -- --fix", 35 | "open:coverage": "open ./coverage/lcov-report/index.html", 36 | "prepare": "husky install", 37 | "prettier": "prettier \"**/*.+(js|ts|json)\"", 38 | "reinstall-node-modules": "bash ./scripts/reinstall-node-modules.sh", 39 | "rlb": "bash ./scripts/remove-local-branches.sh", 40 | "test": "jest --detectOpenHandles --verbose", 41 | "test:ci": "jest --ci --detectOpenHandles --coverage --forceExit", 42 | "test:coverage": "jest --detectOpenHandles --collectCoverage", 43 | "test:update-snapshot": "jest --detectOpenHandles --verbose --updateSnapshot", 44 | "test:watch": "jest --detectOpenHandles --verbose --watch", 45 | "typecheck": "tsc --noEmit" 46 | }, 47 | "types": "dist/index.d.ts", 48 | "dependencies": { 49 | "jsonschema": "^1.4.1" 50 | }, 51 | "devDependencies": { 52 | "@commitlint/cli": "17.5.1", 53 | "@commitlint/config-conventional": "17.4.4", 54 | "@rollup/plugin-babel": "6.0.3", 55 | "@rollup/plugin-commonjs": "24.0.1", 56 | "@rollup/plugin-node-resolve": "15.0.1", 57 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 58 | "@types/jest": "29.5.0", 59 | "@typescript-eslint/eslint-plugin": "5.57.0", 60 | "@typescript-eslint/parser": "5.57.0", 61 | "concurrently": "^7.6.0", 62 | "eslint": "8.36.0", 63 | "eslint-config-airbnb-typescript": "^17.0.0", 64 | "eslint-plugin-folders": "^1.0.4", 65 | "eslint-plugin-import": "^2.27.5", 66 | "eslint-plugin-prettier": "^4.2.1", 67 | "eslint-plugin-sort-destructure-keys": "^1.5.0", 68 | "eslint-plugin-typescript-sort-keys": "^2.3.0", 69 | "eslint-plugin-unicorn": "^46.0.0", 70 | "eslint-plugin-unused-imports": "^2.0.0", 71 | "fake-indexeddb": "4.0.1", 72 | "husky": "^8.0.3", 73 | "jest": "29.5.0", 74 | "jest-module-name-mapper": "0.1.5", 75 | "lint-staged": "13.2.0", 76 | "path": "^0.12.7", 77 | "prettier": "2.8.7", 78 | "prettier-package-json": "2.8.0", 79 | "rimraf": "4.4.1", 80 | "rollup": "3.20.2", 81 | "rollup-plugin-polyfill-node": "0.12.0", 82 | "rollup-plugin-terser": "^7.0.2", 83 | "rollup-plugin-typescript2": "0.34.1", 84 | "ts-jest": "29.0.5", 85 | "tslib": "2.5.0", 86 | "ttypescript": "1.5.15", 87 | "typescript": "4.9.5", 88 | "typescript-transform-paths": "3.4.6" 89 | }, 90 | "keywords": [ 91 | "Paweł", 92 | "Wojtasiński", 93 | "browser", 94 | "browser-fs", 95 | "commands", 96 | "file", 97 | "fs", 98 | "fs-browser", 99 | "fs-web", 100 | "indexed-db", 101 | "indexeddb", 102 | "javascript", 103 | "manager", 104 | "playerony", 105 | "terminal", 106 | "typescript", 107 | "web-fs" 108 | ], 109 | "publishConfig": { 110 | "access": "public" 111 | }, 112 | "husky": { 113 | "hooks": { 114 | "pre-push": "npm test", 115 | "pre-commit": "lint-staged && npm test" 116 | } 117 | }, 118 | "packageManager": "yarn@3.5.0" 119 | } 120 | -------------------------------------------------------------------------------- /lib/framework/create-fs.function.ts: -------------------------------------------------------------------------------- 1 | import { getDatabaseCrud, isIndexedDBSupport } from '@database'; 2 | 3 | import { validateCreateFsProps } from '@utils'; 4 | 5 | import { defaultProps } from './create-fs.defaults'; 6 | import { AnyFunction, ICreateFsOutput, ICreateFsProps } from './create-fs.types'; 7 | import { 8 | copyFileInstance, 9 | createDirectoryInstance, 10 | createRootDirectoryInstance, 11 | detailsInstance, 12 | directoryDetailsInstance, 13 | existsInstance, 14 | fileDetailsInstance, 15 | isDirectoryInstance, 16 | isFileInstance, 17 | moveFileInstance, 18 | readDirectoryInstance, 19 | readFileInstance, 20 | removeDirectoryInstance, 21 | removeFileInstance, 22 | removeInstance, 23 | renameFileInstance, 24 | updateFileDetailsInstance, 25 | writeFileInstance, 26 | } from './parts'; 27 | 28 | const checkIndexedDBSupport = () => { 29 | if (!isIndexedDBSupport()) { 30 | throw new Error('Your browser does not support indexedDB.'); 31 | } 32 | }; 33 | 34 | export const createFs = ({ 35 | databaseName = defaultProps.databaseName, 36 | databaseVersion = defaultProps.databaseVersion, 37 | objectStoreName = defaultProps.objectStoreName, 38 | rootDirectoryName = defaultProps.rootDirectoryName, 39 | }: ICreateFsProps = defaultProps): ICreateFsOutput => { 40 | const validateProps = () => 41 | validateCreateFsProps({ 42 | databaseName, 43 | objectStoreName, 44 | databaseVersion, 45 | rootDirectoryName, 46 | }); 47 | 48 | const initialize = () => { 49 | checkIndexedDBSupport(); 50 | validateProps(); 51 | }; 52 | 53 | const { deleteRecord, getRecord, openCursor, putRecord } = getDatabaseCrud({ 54 | databaseName, 55 | databaseVersion, 56 | objectStoreName, 57 | }); 58 | 59 | const exists: (fullPath: string) => Promise = existsInstance({ 60 | getRecord, 61 | rootDirectoryName, 62 | }); 63 | 64 | const isFile = isFileInstance({ 65 | exists, 66 | getRecord, 67 | rootDirectoryName, 68 | }); 69 | 70 | const remove = removeInstance({ 71 | exists, 72 | deleteRecord, 73 | rootDirectoryName, 74 | }); 75 | 76 | const fileDetails = fileDetailsInstance({ 77 | isFile, 78 | getRecord, 79 | rootDirectoryName, 80 | }); 81 | 82 | const readFile = readFileInstance({ 83 | fileDetails, 84 | }); 85 | 86 | const removeFile = removeFileInstance({ 87 | isFile, 88 | deleteRecord, 89 | rootDirectoryName, 90 | }); 91 | 92 | const isDirectory = isDirectoryInstance({ 93 | exists, 94 | getRecord, 95 | rootDirectoryName, 96 | }); 97 | 98 | const writeFile = writeFileInstance({ 99 | putRecord, 100 | isDirectory, 101 | rootDirectoryName, 102 | }); 103 | 104 | const copyFile = copyFileInstance({ 105 | exists, 106 | isFile, 107 | writeFile, 108 | fileDetails, 109 | isDirectory, 110 | rootDirectoryName, 111 | }); 112 | 113 | const readDirectory = readDirectoryInstance({ 114 | openCursor, 115 | isDirectory, 116 | rootDirectoryName, 117 | }); 118 | 119 | const createDirectory = createDirectoryInstance({ 120 | isFile, 121 | putRecord, 122 | isDirectory, 123 | rootDirectoryName, 124 | }); 125 | 126 | const removeDirectory = removeDirectoryInstance({ 127 | remove, 128 | isDirectory, 129 | readDirectory, 130 | rootDirectoryName, 131 | }); 132 | 133 | const directoryDetails = directoryDetailsInstance({ 134 | getRecord, 135 | isDirectory, 136 | rootDirectoryName, 137 | }); 138 | 139 | const details = detailsInstance({ 140 | isFile, 141 | exists, 142 | isDirectory, 143 | fileDetails, 144 | directoryDetails, 145 | rootDirectoryName, 146 | }); 147 | 148 | const updateFileDetails = updateFileDetailsInstance({ 149 | putRecord, 150 | fileDetails, 151 | isDirectory, 152 | rootDirectoryName, 153 | }); 154 | 155 | const moveFile = moveFileInstance({ 156 | exists, 157 | isFile, 158 | removeFile, 159 | isDirectory, 160 | updateFileDetails, 161 | rootDirectoryName, 162 | }); 163 | 164 | const renameFile = renameFileInstance({ 165 | exists, 166 | isFile, 167 | removeFile, 168 | updateFileDetails, 169 | rootDirectoryName, 170 | }); 171 | 172 | const createRootDirectory = createRootDirectoryInstance({ 173 | putRecord, 174 | rootDirectoryName, 175 | }); 176 | 177 | const createRootDirectoryIfDoesNotExist = async (): Promise => { 178 | const hasRootDirectory = await exists(rootDirectoryName); 179 | 180 | if (!hasRootDirectory) { 181 | await createRootDirectory(); 182 | } 183 | }; 184 | 185 | const withRootDirectoryCheck = 186 | (callback: TFunction) => 187 | async (...args: Parameters) => { 188 | await createRootDirectoryIfDoesNotExist(); 189 | 190 | return callback(...args); 191 | }; 192 | 193 | initialize(); 194 | 195 | return { 196 | databaseName, 197 | databaseVersion, 198 | objectStoreName, 199 | rootDirectoryName, 200 | exists: withRootDirectoryCheck(exists), 201 | isFile: withRootDirectoryCheck(isFile), 202 | remove: withRootDirectoryCheck(remove), 203 | details: withRootDirectoryCheck(details), 204 | copyFile: withRootDirectoryCheck(copyFile), 205 | readFile: withRootDirectoryCheck(readFile), 206 | moveFile: withRootDirectoryCheck(moveFile), 207 | writeFile: withRootDirectoryCheck(writeFile), 208 | renameFile: withRootDirectoryCheck(renameFile), 209 | removeFile: withRootDirectoryCheck(removeFile), 210 | fileDetails: withRootDirectoryCheck(fileDetails), 211 | isDirectory: withRootDirectoryCheck(isDirectory), 212 | readDirectory: withRootDirectoryCheck(readDirectory), 213 | createDirectory: withRootDirectoryCheck(createDirectory), 214 | removeDirectory: withRootDirectoryCheck(removeDirectory), 215 | directoryDetails: withRootDirectoryCheck(directoryDetails), 216 | }; 217 | }; 218 | -------------------------------------------------------------------------------- /lib/framework/create-fs.spec.ts: -------------------------------------------------------------------------------- 1 | import { functionImportTest } from '@utils'; 2 | 3 | import { createFs } from './create-fs.function'; 4 | 5 | describe('createFs Function', () => { 6 | functionImportTest(createFs); 7 | 8 | it('should throw an error when the browser does not support indexedDB', () => { 9 | const copiedIndexedDB = indexedDB; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | indexedDB = null as any; 13 | 14 | expect(() => createFs()).toThrowErrorMatchingSnapshot(); 15 | 16 | // eslint-disable-next-line no-global-assign 17 | indexedDB = copiedIndexedDB; 18 | }); 19 | 20 | describe('fs object creation', () => { 21 | it('should throw an error when passed configuration is invalid', () => { 22 | expect(() => 23 | createFs({ 24 | databaseVersion: 1.5, 25 | rootDirectoryName: '', 26 | objectStoreName: ' starts with space', 27 | databaseName: 'too long database name as example of databaseName parameter', 28 | }), 29 | ).toThrowErrorMatchingSnapshot(); 30 | }); 31 | 32 | it('should return an fs object with the default configuration', () => { 33 | const { databaseName, databaseVersion, objectStoreName, rootDirectoryName } = createFs(); 34 | 35 | expect(databaseVersion).toEqual(1); 36 | expect(objectStoreName).toEqual('files'); 37 | expect(rootDirectoryName).toEqual('root'); 38 | expect(databaseName).toEqual('indexeddb-fs'); 39 | }); 40 | }); 41 | 42 | describe('root directory case', () => { 43 | it('should create root directory as default', async () => { 44 | const { isDirectory } = createFs(); 45 | 46 | await expect(isDirectory('root')).resolves.toBeTruthy(); 47 | }); 48 | 49 | it('user should be able to remove root directory', async () => { 50 | const { createDirectory, exists, removeDirectory, rootDirectoryName, writeFile } = createFs(); 51 | 52 | await createDirectory('files'); 53 | await createDirectory('directories'); 54 | 55 | await writeFile('file.txt', 'file'); 56 | await writeFile('files/file.txt', 'file 2'); 57 | 58 | await removeDirectory(rootDirectoryName); 59 | await expect(exists('root')).resolves.toBeTruthy(); 60 | await expect(exists('file.txt')).resolves.toBeFalsy(); 61 | await expect(exists('root/files')).resolves.toBeFalsy(); 62 | await expect(exists('files/file.txt')).resolves.toBeFalsy(); 63 | }); 64 | 65 | it('user should be able to read root directory', async () => { 66 | const { createDirectory, readDirectory, rootDirectoryName, writeFile } = createFs(); 67 | 68 | await writeFile('/file.txt', 'content'); 69 | await writeFile('root/file1.txt', 'content'); 70 | await createDirectory('example_directory'); 71 | 72 | const { directoriesCount, filesCount, isEmpty } = await readDirectory(rootDirectoryName); 73 | 74 | expect(isEmpty).toBeFalsy(); 75 | expect(filesCount).toEqual(2); 76 | expect(directoriesCount).toEqual(1); 77 | }); 78 | }); 79 | 80 | it('user should be able to create and remove directory with given name', async () => { 81 | const { 82 | createDirectory, 83 | exists, 84 | isDirectory, 85 | isFile, 86 | readDirectory, 87 | removeDirectory, 88 | rootDirectoryName, 89 | writeFile, 90 | } = createFs(); 91 | 92 | await createDirectory('files'); 93 | await createDirectory('/files/private'); 94 | await createDirectory(`${rootDirectoryName}/files/public`); 95 | 96 | await expect(isDirectory('files')).resolves.toBeTruthy(); 97 | await expect(isDirectory('/files/private')).resolves.toBeTruthy(); 98 | await expect(isDirectory(rootDirectoryName)).resolves.toBeTruthy(); 99 | await expect(isDirectory('root/files/private')).resolves.toBeTruthy(); 100 | await expect(isDirectory(`${rootDirectoryName}/files/private`)).resolves.toBeTruthy(); 101 | 102 | await writeFile('files/public/file.txt', 'content'); 103 | await expect(isFile('files/public/file.txt')).resolves.toBeTruthy(); 104 | await expect(exists('files/public/file.txt')).resolves.toBeTruthy(); 105 | 106 | await removeDirectory(rootDirectoryName); 107 | const { directoriesCount, filesCount } = await readDirectory(rootDirectoryName); 108 | 109 | expect(filesCount).toEqual(0); 110 | expect(directoriesCount).toEqual(0); 111 | }); 112 | 113 | it('user should be able to move, copy and rename files', async () => { 114 | const { 115 | copyFile, 116 | createDirectory, 117 | exists, 118 | fileDetails, 119 | isFile, 120 | moveFile, 121 | readDirectory, 122 | readFile, 123 | removeDirectory, 124 | removeFile, 125 | renameFile, 126 | rootDirectoryName, 127 | writeFile, 128 | } = createFs(); 129 | 130 | await createDirectory('files'); 131 | await createDirectory('copied_files'); 132 | await writeFile('files/file.txt', 'content'); 133 | 134 | await copyFile('files/file.txt', 'copied_files/copied_file.txt'); 135 | 136 | await expect(isFile('files/file.txt')).resolves.toBeTruthy(); 137 | await expect(isFile('copied_files/copied_file.txt')).resolves.toBeTruthy(); 138 | 139 | await removeFile('files/file.txt'); 140 | 141 | await renameFile('copied_files/copied_file.txt', 'file.txt'); 142 | await expect(exists('copied_files/file.txt')).resolves.toBeTruthy(); 143 | await expect(exists('copied_files/copied_file.txt')).resolves.toBeFalsy(); 144 | 145 | await moveFile('copied_files/file.txt', 'files/file.txt'); 146 | await expect(isFile('files/file.txt')).resolves.toBeTruthy(); 147 | await expect(exists('copied_files/file.txt')).resolves.toBeFalsy(); 148 | 149 | await moveFile('files/file.txt', 'file.txt'); 150 | await removeDirectory('files'); 151 | await removeDirectory('copied_files'); 152 | 153 | const { directoriesCount, filesCount } = await readDirectory(rootDirectoryName); 154 | 155 | expect(filesCount).toEqual(1); 156 | expect(directoriesCount).toEqual(0); 157 | 158 | const details = await fileDetails('file.txt'); 159 | 160 | expect(details.type).toEqual('file'); 161 | expect(details.data).toEqual('content'); 162 | expect(details.name).toEqual('file.txt'); 163 | expect(details.fullPath).toEqual('root/file.txt'); 164 | expect(details.directory).toEqual(rootDirectoryName); 165 | 166 | await expect(readFile('file.txt')).resolves.toEqual('content'); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # indexeddb-fs 2 | 3 | An **fs** library for the browser that lets you store data using an API similar to Node's [fs module](http://nodejs.org/api/fs.html). It's powered by [IndexedDB](http://www.w3.org/TR/IndexedDB/), a browser database. 4 | 5 | [![npm](https://img.shields.io/npm/v/indexeddb-fs.svg)](https://www.npmjs.com/package/indexeddb-fs) 6 | ![types](https://img.shields.io/badge/types-typescript%20%7C%20flow-blueviolet) 7 | [![minzip](https://img.shields.io/bundlephobia/minzip/indexeddb-fs.svg)](https://www.npmjs.com/package/indexeddb-fs) 8 | [![downloads per month](https://img.shields.io/npm/dm/indexeddb-fs.svg)](https://www.npmjs.com/package/indexeddb-fs) 9 | [![issues](https://img.shields.io/github/issues/playerony/indexeddb-fs.svg)](https://www.npmjs.com/package/indexeddb-fs) 10 | [![license](https://img.shields.io/github/license/playerony/indexeddb-fs)](https://www.npmjs.com/package/indexeddb-fs) 11 | 12 | ## Motivation 13 | 14 | Other solutions I found didn't work well. They lacked validation and didn't create directories properly. 15 | 16 | ## Installation 17 | 18 | ```shell 19 | npm install indexeddb-fs 20 | ``` 21 | 22 | ## A simple example of how you can use indexeddb-fs 23 | 24 | ```js 25 | import fs from 'indexeddb-fs'; 26 | 27 | async function main() { 28 | // Check if a directory exists 29 | const directoryExists = await fs.isDirectory('my_directory'); 30 | 31 | // Create a new directory if it doesn't exist 32 | if (!directoryExists) { 33 | await fs.createDirectory('my_directory'); 34 | } 35 | 36 | // Write data to a file 37 | const content = 'Hello, world!'; 38 | await fs.writeFile('my_directory/my_file.txt', content); 39 | 40 | // Read data from the file 41 | const readContent = await fs.readFile('my_directory/my_file.txt'); 42 | console.log(readContent); // "Hello, world!" 43 | 44 | // Remove the directory and all files within it 45 | await fs.removeDirectory('my_directory'); 46 | } 47 | 48 | main(); 49 | ``` 50 | 51 | ## A more complex example that demonstrates the use of all the functions 52 | 53 | ```js 54 | import fs from 'indexeddb-fs'; 55 | 56 | async function main() { 57 | // Create a new directory and subdirectories 58 | await fs.createDirectory('my_directory'); 59 | await fs.createDirectory('my_directory/subdirectory'); 60 | await fs.createDirectory('my_directory/another_subdirectory'); 61 | 62 | // Write some content to a file 63 | const content = 'Hello, world!'; 64 | await fs.writeFile('my_directory/my_file.txt', content); 65 | 66 | // Check if a file exists and if it's a file 67 | const fileExists = await fs.exists('my_directory/my_file.txt'); 68 | const isAFile = await fs.isFile('my_directory/my_file.txt'); 69 | 70 | console.log('File exists:', fileExists); // true 71 | console.log('Is a file:', isAFile); // true 72 | 73 | // Copy the file to a new location 74 | await fs.copyFile('my_directory/my_file.txt', 'my_directory/another_subdirectory/my_file_copy.txt'); 75 | 76 | // Rename and move the original file to a new location 77 | await fs.renameFile('my_directory/my_file.txt', 'my_directory/new_file_name.txt'); 78 | await fs.moveFile('my_directory/new_file_name.txt', 'my_directory/subdirectory/new_location.txt'); 79 | 80 | // Read the contents of a directory and count the number of files and subdirectories 81 | const { filesCount, directoriesCount } = await fs.readDirectory('my_directory'); 82 | 83 | console.log('Number of files:', filesCount); // 0 84 | console.log('Number of subdirectories:', directoriesCount); // 2 85 | 86 | // Remove the directory and all files within it 87 | await fs.removeDirectory('my_directory'); 88 | 89 | // Read the contents of the copied file 90 | const copiedFileContent = await fs.readFile('my_directory/another_subdirectory/my_file_copy.txt'); 91 | 92 | console.log('Copied file content:', copiedFileContent); // "Hello, world!" 93 | } 94 | 95 | main(); 96 | ``` 97 | 98 | ### Custom fs object 99 | 100 | ```js 101 | import { createFs } from 'indexeddb-fs'; 102 | 103 | const fs = createFs({ 104 | databaseVersion: 'indexeddb version (default "1")', 105 | objectStoreName: 'store name in indexeddb (default "files")', 106 | rootDirectoryName: 'your root directory name (default "root") ', 107 | databaseName: 'indexeddb database name (default "indexeddb-fs")', 108 | }); 109 | ``` 110 | 111 | # Api 112 | 113 | - All methods in indexeddb-fs return a Promise since IndexedDB is an asynchronous API. 114 | - Each method can reject the returned Promise with an `Error` object when an error occurs. 115 | - It's important to handle errors appropriately in your code to ensure your application is robust and doesn't break unexpectedly. 116 | 117 | # Fields 118 | 119 | The indexeddb-fs library contains several configuration fields that can be accessed after importing the library. These fields include: 120 | 121 | - `databaseName`: the name of the IndexedDB database used by the library. 122 | - `databaseVersion`: the version number of the IndexedDB database used by the library. 123 | - `objectStoreName`: the name of the object store used by the library to store data. 124 | - `rootDirectoryName`: the name of the root directory used by the library. 125 | 126 | To access these fields, import them from the `indexeddb-fs` module as shown in the example code above. These fields contain configuration information for the library and can also be used to access default values. 127 | 128 | # Common functions 129 | 130 | ## fs.exists(fullPath) 131 | 132 | - Parameters: [`fullPath`: string] 133 | - Returns: `Promise` 134 | - Description: Returns a promise that resolves to `true` if the file or directory at the given `fullPath` exists, and `false` otherwise. 135 | 136 | Example of usage: 137 | 138 | ```js 139 | // Check if a file exists 140 | const fileExists = await fs.exists('path/to/file.txt'); 141 | if (fileExists) { 142 | console.log('The file exists!'); 143 | } else { 144 | console.log('The file does not exist.'); 145 | } 146 | 147 | // Check if a directory exists 148 | const dirExists = await fs.exists('path/to/directory'); 149 | if (dirExists) { 150 | console.log('The directory exists!'); 151 | } else { 152 | console.log('The directory does not exist.'); 153 | } 154 | ``` 155 | 156 | ## fs.remove(fullPath) 157 | 158 | - Parameters: [`fullPath`: string] 159 | - Returns: `Promise` 160 | - Description: Removes the file or directory at the given `fullPath`. The method does not remove directories recursively, so it will throw an error if the path is not empty. 161 | 162 | Example of usage: 163 | 164 | ```js 165 | // Remove a file 166 | await fs.writeFile('file1.txt', 'test content'); 167 | await fs.remove('file1.txt'); 168 | await fs.exists('file1.txt').then((result) => { 169 | console.log(!result); 170 | }); 171 | 172 | // Remove an empty directory 173 | await fs.createDirectory('directory1'); 174 | await fs.remove('directory1'); 175 | await fs.exists('directory1').then((result) => { 176 | console.log(!result); 177 | }); 178 | 179 | // Attempt to remove a non-empty directory 180 | await fs.createDirectory('directory2'); 181 | await fs.writeFile('directory2/file2.txt', 'test content'); 182 | await fs.remove('directory2').catch((error) => { 183 | console.error(error.message); 184 | }); 185 | await fs.exists('directory2').then((result) => { 186 | console.log(result); 187 | }); 188 | ``` 189 | 190 | ## fs.details(fullPath) 191 | 192 | - Parameters: [`fullPath`: string] 193 | - Returns: `Promise | DirectoryEntry>` 194 | - Description: Returns an object containing details about the file or directory at the given `fullPath`. The object contains the following properties: 195 | - `type`: The type of the entry (file or directory). 196 | - `name`: The name of the entry. 197 | - `directory`: The name of the directory that contains the entry. 198 | - `fullPath`: The full path of the entry, including the directory. 199 | - Throws an error when the path does not contain anything. 200 | 201 | Example of usage: 202 | 203 | ```js 204 | // Get details of a newly created directory 205 | const createdDirectory = await fs.createDirectory('directory'); 206 | const createdDirectoryDetails = await fs.details(createdDirectory.fullPath); 207 | 208 | console.log('Type:', createdDirectoryDetails.type); // directory 209 | console.log('Name:', createdDirectoryDetails.name); // directory 210 | console.log('Directory:', createdDirectoryDetails.directory); // root 211 | console.log('Full Path:', createdDirectoryDetails.fullPath); // root/directory 212 | ``` 213 | 214 | Example result for `FileEntry` type: 215 | 216 | ```object 217 | { 218 | type: 'file', 219 | name: 'file.txt', 220 | directory: 'root', 221 | data: 'test 2 content', 222 | createdAt: 1626882161631, 223 | fullPath: 'root/file.txt' 224 | } 225 | ``` 226 | 227 | Example result for `DirectoryEntry` type: 228 | 229 | ```object 230 | { 231 | isRoot: false, 232 | directory: 'root', 233 | type: 'directory', 234 | name: 'directory', 235 | createdAt: 1626882291087, 236 | fullPath: 'root/directory' 237 | } 238 | ``` 239 | 240 | # File functions 241 | 242 | ## fs.isFile(fullPath) 243 | 244 | - Parameters: [`fullPath`: string] 245 | - Returns: `Promise` 246 | - Description: Returns a promise that resolves to `true` if a file exists at the given `fullPath`, and `false` otherwise. If the path does not contain anything, an error is thrown. 247 | 248 | Example of usage: 249 | 250 | ```js 251 | import fs from 'indexeddb-fs'; 252 | 253 | // Create directories for testing 254 | await fs.createDirectory('files'); 255 | await fs.createDirectory('directories'); 256 | 257 | // Check if directories are files 258 | await fs.isFile('files').then((result) => { 259 | console.log(result); 260 | }); 261 | await fs.isFile('directories').then((result) => { 262 | console.log(result); 263 | }); 264 | 265 | // Create a file and check if it is a file 266 | await fs.writeFile('file', 'content'); 267 | await fs.isFile('file').then((result) => { 268 | console.log(result); 269 | }); 270 | 271 | // Create a file in the 'files' directory and check if it is a file 272 | await fs.writeFile('files/file', 'content'); 273 | await fs.isFile('files/file').then((result) => { 274 | console.log(result); 275 | }); 276 | ``` 277 | 278 | Example result for `FileEntry` type: 279 | 280 | ```object 281 | { 282 | type: 'file', 283 | name: 'file.txt', 284 | directory: 'root', 285 | data: 'test 2 content', 286 | createdAt: 1626882161631, 287 | fullPath: 'root/file.txt' 288 | } 289 | ``` 290 | 291 | ## fs.writeFile(fullPath, data) 292 | 293 | - Parameters: [`fullPath`: string, `data`: TData] 294 | - Returns: `Promise>` 295 | - Description: Writes `data` to the file specified by `fullPath`, replacing the file if it already exists. The `data` parameter can be any data you want to write to the file, and is usually a string or an object that can be serialized to JSON. The method returns a promise that resolves to a `FileEntry` object representing the file that was written. 296 | - Throws an error when the destination directory of the file does not exist. This can happen if the path to the file contains one or more directories that do not exist yet. You can use the `createDirectory` method to create the missing directories. 297 | - Throws an error when the path contains a directory with the same name as the file. For example, if a directory named "file.txt" exists, you cannot create a file with the same name in the same directory. 298 | 299 | Example of usage: 300 | 301 | ```js 302 | import fs from 'indexeddb-fs'; 303 | 304 | // Create a directory to write the file to 305 | await fs.createDirectory('my_directory'); 306 | 307 | // Write some data to a file 308 | const fileEntry = await fs.writeFile('my_directory/my_file.txt', 'Hello, world!'); 309 | 310 | console.log(fileEntry); // FileEntry object representing the file that was written 311 | ``` 312 | 313 | Example result for `FileEntry` type: 314 | 315 | ```object 316 | { 317 | type: 'file', 318 | name: 'file.txt', 319 | directory: 'root', 320 | data: 'test 2 content', 321 | createdAt: 1626882161631, 322 | fullPath: 'root/file.txt' 323 | } 324 | ``` 325 | 326 | ## fs.fileDetails(fullPath) 327 | 328 | - Parameters: [`fullPath`: string] 329 | - Returns: `Promise>` 330 | - Description: Returns a promise that resolves to a `FileEntry` object containing details about the file specified by `fullPath`. The `FileEntry` object includes information such as the file's name, size, modification date, and type. If the file does not exist, or if the path contains directories that do not exist, the method will throw an error. 331 | - Throws an error when the path does not contain anything. This can happen if the `fullPath` parameter is an empty string or `null`. 332 | - Throws an error when the destination file is not a file. For example, if the path points to a directory, the method will throw an error. 333 | 334 | Example of usage: 335 | 336 | ```js 337 | import fs from 'indexeddb-fs'; 338 | 339 | // Get details about a file 340 | const fileEntry = await fs.fileDetails('my_directory/my_file.txt'); 341 | 342 | console.log(fileEntry); // FileEntry object representing the file 343 | ``` 344 | 345 | ## fs.readFile(fullPath) 346 | 347 | - Parameters: [`fullPath`: string] 348 | - Returns: `Promise` 349 | - Description: Returns a promise that resolves to the contents of the file specified by `fullPath`. The returned data type is the same type with which it was saved. For example, if the file was saved as a string using the `writeFile` method, the returned data type will also be a string. If the file was saved as an object using the `writeFile` method, the returned data type will also be an object. 350 | - Throws an error when the destination file does not exist. This can happen if the file was deleted or if the `fullPath` parameter points to a file that does not exist yet. 351 | - Throws an error when the destination file is not a file. For example, if the path points to a directory, the method will throw an error. 352 | 353 | Example of usage: 354 | 355 | ```js 356 | import fs from 'indexeddb-fs'; 357 | 358 | // Read the contents of a file 359 | const fileContents = await fs.readFile('my_directory/my_file.txt'); 360 | 361 | console.log(fileContents); // Contents of the file 362 | ``` 363 | 364 | ## fs.removeFile(fullPath) 365 | 366 | - Parameters: [`fullPath`: string] 367 | - Returns: `Promise` 368 | - Description: Removes the file specified by `fullPath`. The method returns a promise that resolves once the file has been successfully removed. 369 | - Throws an error when the path does not contain anything. This can happen if the `fullPath` parameter is an empty string or `null`. 370 | - Throws an error when the destination file is not a file. For example, if the path points to a directory, the method will throw an error. 371 | 372 | Example of usage: 373 | 374 | ```js 375 | import fs from 'indexeddb-fs'; 376 | 377 | // Remove a file 378 | await fs.removeFile('my_directory/my_file.txt'); 379 | ``` 380 | 381 | ## fs.renameFile(fullPath, newFilename) 382 | 383 | - Parameters: [`fullPath`: string, `newFilename`: string] 384 | - Returns: `Promise>` 385 | - Description: Renames the file specified by `fullPath` to the new filename provided as `newFilename`. The method returns a promise that resolves to a `FileEntry` object representing the renamed file. 386 | - Throws an error when the path does not contain anything. This can happen if the `fullPath` parameter is an empty string or `null`. 387 | - Throws an error when the `fullPath` is not a file. For example, if the path points to a directory, the method will throw an error. 388 | - Throws an error when the `fullPath` with a `newFilename` is already taken. For example, if a file named `newFilename` already exists in the same directory as the original file, the method will throw an error. 389 | 390 | Example of usage: 391 | 392 | ```js 393 | import fs from 'indexeddb-fs'; 394 | 395 | // Rename a file 396 | const renamedFile = await fs.renameFile('my_directory/my_file.txt', 'new_file.txt'); 397 | 398 | console.log(renamedFile); // FileEntry object representing the renamed file 399 | ``` 400 | 401 | Example result for `FileEntry` type: 402 | 403 | ```object 404 | { 405 | type: 'file', 406 | name: 'file.txt', 407 | directory: 'root', 408 | data: 'test 2 content', 409 | createdAt: 1626882161631, 410 | fullPath: 'root/file.txt' 411 | } 412 | ``` 413 | 414 | ## fs.copyFile(fullPath, destinationPath) 415 | 416 | - Parameters: [`fullPath`: string, `destinationPath`: string] 417 | - Returns: `Promise>` 418 | - Description: Copies the file specified by `fullPath` to the destination path specified by `destinationPath`. The method returns a promise that resolves to a `FileEntry` object representing the copied file at the new `destinationPath`. 419 | - Throws an error when the path does not contain anything. This can happen if the `fullPath` or `destinationPath` parameters are empty strings or `null`. 420 | - Throws an error when the `fullPath` is not a file. For example, if the path points to a directory, the method will throw an error. 421 | - Throws an error when the `destinationPath` is already taken. For example, if a file or directory already exists at the `destinationPath`, the method will throw an error. 422 | 423 | Example of usage: 424 | 425 | ```js 426 | import fs from 'indexeddb-fs'; 427 | 428 | // Copy a file 429 | const copiedFile = await fs.copyFile('my_directory/my_file.txt', 'my_directory/copied_file.txt'); 430 | 431 | console.log(copiedFile); // FileEntry object representing the copied file 432 | ``` 433 | 434 | Example result for `FileEntry` type: 435 | 436 | ```object 437 | { 438 | type: 'file', 439 | name: 'file.txt', 440 | directory: 'root', 441 | data: 'test 2 content', 442 | createdAt: 1626882161631, 443 | fullPath: 'root/file.txt' 444 | } 445 | ``` 446 | 447 | ## fs.moveFile(fullPath, destinationPath) 448 | 449 | - Parameters: [`fullPath`: string, `destinationPath`: string] 450 | - Returns: `Promise>` 451 | - Description: Moves the file specified by `fullPath` to the destination path specified by `destinationPath`. The method returns a promise that resolves to a `FileEntry` object representing the moved file at the new `destinationPath`. 452 | - Throws an error when the path does not contain anything. This can happen if the `fullPath` or `destinationPath` parameters are empty strings or `null`. 453 | - Throws an error when the `fullPath` is not a file. For example, if the path points to a directory, the method will throw an error. 454 | - Throws an error when the `destinationPath` is already taken. For example, if a file or directory already exists at the `destinationPath`, the method will throw an error. 455 | 456 | Example of usage: 457 | 458 | ```js 459 | import fs from 'indexeddb-fs'; 460 | 461 | // Move a file 462 | const movedFile = await fs.moveFile('my_directory/my_file.txt', 'my_directory/moved_file.txt'); 463 | 464 | console.log(movedFile); // FileEntry object representing the moved file 465 | ``` 466 | 467 | Example result for `FileEntry` type: 468 | 469 | ```object 470 | { 471 | type: 'file', 472 | name: 'file.txt', 473 | directory: 'root', 474 | data: 'test 2 content', 475 | createdAt: 1626882161631, 476 | fullPath: 'root/file.txt' 477 | } 478 | ``` 479 | 480 | # Directory functions 481 | 482 | ## fs.isDirectory(fullPath) 483 | 484 | - Parameters: [`fullPath`: string] 485 | - Returns: `Promise` 486 | - Description: Returns `true` if the path specified by `fullPath` contains a directory, `false` otherwise. 487 | - Throws an error when the path does not contain anything. This can happen if the `fullPath` parameter is an empty string or `null`. 488 | 489 | Example of usage: 490 | 491 | ```js 492 | import fs from 'indexeddb-fs'; 493 | 494 | // Check if a path is a directory 495 | const isDirectory = await fs.isDirectory('my_directory'); 496 | 497 | console.log(isDirectory); // true if 'my_directory' is a directory, false otherwise 498 | ``` 499 | 500 | ## fs.createDirectory(fullPath) 501 | 502 | - Parameters: [`fullPath`: string] 503 | - Returns: `Promise` 504 | - Description: Creates a new directory at the path specified by `fullPath` and returns a Promise that resolves to a `DirectoryEntry` object representing the new directory. 505 | - Throws an error when the destination directory of the directory does not exist. For example, if the `fullPath` parameter specifies a nested directory structure, the method will throw an error if any of the parent directories do not already exist. 506 | - Throws an error when the path contains a file with the same name. For example, if a file called `'my_directory'` already exists in the specified path, the method will throw an error. 507 | 508 | Example of usage: 509 | 510 | ```js 511 | import fs from 'indexeddb-fs'; 512 | 513 | // Create a new directory 514 | const newDirectory = await fs.createDirectory('my_directory'); 515 | 516 | console.log(newDirectory); // DirectoryEntry object representing the new directory 517 | ``` 518 | 519 | Example result for `DirectoryEntry` type: 520 | 521 | ```object 522 | { 523 | isRoot: false, 524 | directory: 'root', 525 | type: 'directory', 526 | name: 'directory', 527 | createdAt: 1626882291087, 528 | fullPath: 'root/directory' 529 | } 530 | ``` 531 | 532 | ## fs.readDirectory(fullPath) 533 | 534 | - Parameters: [`fullPath`: string] 535 | - Returns: `Promise` 536 | - Description: Reads the entire contents of the directory specified by `fullPath` and returns a Promise that resolves to an object containing an array of `DirectoryEntry` and `FileEntry` objects representing the contents of the directory, as well as a count of the number of directories and files in the directory. 537 | - Throws an error when the destination directory does not exist. For example, if the `fullPath` parameter specifies a nested directory structure, the method will throw an error if any of the parent directories do not exist. 538 | - Throws an error when the destination directory is not a directory. For example, if the `fullPath` parameter specifies a file rather than a directory, the method will throw an error. 539 | 540 | Example of usage: 541 | 542 | ```js 543 | import fs from 'indexeddb-fs'; 544 | 545 | // Read the contents of a directory 546 | const directoryContents = await fs.readDirectory('my_directory'); 547 | 548 | console.log(directoryContents); 549 | // { files: [...], directories: [...], filesCount: 2, directoriesCount: 1 } 550 | ``` 551 | 552 | Example result for `ReadDirectoryInstanceOutput` type: 553 | 554 | ```object 555 | { 556 | files: [], 557 | filesCount: 0, 558 | isEmpty: true, 559 | directories: [], 560 | directoriesCount: 0, 561 | } 562 | ``` 563 | 564 | ## fs.directoryDetails(fullPath) 565 | 566 | - Parameters: [`fullPath`: string] 567 | - Returns: `Promise` 568 | - Description: Returns an object with details about the directory specified by `fullPath`. The method returns a Promise that resolves to a `DirectoryEntry` object representing the directory. 569 | - Throws an error when the path does not contain anything. For example, if the `fullPath` parameter is an empty string or undefined, the method will throw an error. 570 | - Throws an error when the destination directory is not a directory. For example, if the `fullPath` parameter specifies a file rather than a directory, the method will throw an error. 571 | 572 | Example of usage: 573 | 574 | ```js 575 | import fs from 'indexeddb-fs'; 576 | 577 | // Get details about a directory 578 | const directory = await fs.directoryDetails('my_directory'); 579 | 580 | console.log(directory); 581 | // DirectoryEntry object representing the directory 582 | ``` 583 | 584 | Example result for `DirectoryEntry` type: 585 | 586 | ```object 587 | { 588 | isRoot: false, 589 | directory: 'root', 590 | type: 'directory', 591 | name: 'directory', 592 | createdAt: 1626882291087, 593 | fullPath: 'root/directory' 594 | } 595 | ``` 596 | 597 | ## fs.removeDirectory(fullPath) 598 | 599 | - Parameters: [`fullPath`: string] 600 | - Returns: `Promise` 601 | - Description: Removes the directory specified by `fullPath`, recursively removing any files/subdirectories contained within. The method returns a Promise that resolves once the directory has been removed. 602 | - Throws an error when the destination directory does not exist. For example, if the `fullPath` parameter specifies a nested directory structure, the method will throw an error if any of the parent directories do not exist. 603 | - Throws an error when the destination directory is not a directory. For example, if the `fullPath` parameter specifies a file rather than a directory, the method will throw an error. 604 | 605 | Example of usage: 606 | 607 | ```js 608 | import fs from 'indexeddb-fs'; 609 | 610 | // Remove a directory 611 | await fs.removeDirectory('my_directory'); 612 | ``` 613 | 614 | # License 615 | 616 | This project is licensed under the MIT License - see the LICENSE file for details. 617 | --------------------------------------------------------------------------------