import fs from 'fs'; import path from 'path'; import { format, writeToPath } from '@fast-csv/format'; import { parse } from '@fast-csv/parse'; type Stringify = { [key in keyof T]: string; }; type Textify = { [key in keyof T]: string | number; }; export async function exists(filePath: string) { try { await fs.promises.access(filePath, fs.constants.F_OK); return true; } catch { return false; } } export async function mkdir(path: string) { try { await fs.promises.mkdir(path, { recursive: true }); } catch {} } export async function rmFile(path: string) { try { await fs.promises.rm(path); } catch {} } export interface IStorage

{ read(): Promise

; write(item: P): Promise; } export interface IListStorage

{ append(row: P | P[]): Promise; } export abstract class BaseFileStorage

implements IStorage

{ protected constructor(public folderPath: string, public fileName: string) {} public async delete(): Promise { await rmFile(this.filePath); } public async exists(filePath: string = this.filePath) { return await exists(filePath); } protected async createFolder() { await fs.promises.mkdir(this.folderPath, { recursive: true }); } protected get filePath() { return path.resolve(this.folderPath, this.fileName); } abstract read(): Promise

; abstract write(item: P): Promise; } export abstract class BaseListFileStorage

extends BaseFileStorage implements IListStorage

{ public abstract append(row: P | P[]): Promise; } export class JsonStorage

extends BaseFileStorage

{ constructor(public folderPath: string, public fileName: string) { super(folderPath, fileName); } async write(data: P): Promise { await this.createFolder(); const str = JSON.stringify(data); await fs.promises.writeFile(this.filePath, str, { encoding: 'utf-8' }); } async read(): Promise

{ if (!(await exists(this.filePath))) { return null; } const str = await fs.promises.readFile(this.filePath, { encoding: 'utf-8', }); return JSON.parse(str); } } export class CsvStorage

extends BaseListFileStorage

{ constructor( public readonly folderPath: string, public readonly fileName: string, protected readonly onRead: (row: Stringify

) => P, protected readonly onWrite: (row: P) => Textify

, ) { super(folderPath, fileName); } public async read(): Promise { if (!(await this.exists(this.filePath))) { return null; } return new Promise((res, rej) => { const data: P[] = []; const readStream = fs.createReadStream(this.filePath, { encoding: 'utf-8', }); const csvStream = parse({ headers: true }) .on('data', (row) => { data.push(this.onRead(row)); }) .on('end', () => res(data)) .on('error', rej); readStream.pipe(csvStream); }); } public async write(rows: P[]): Promise { return new Promise(async (res, rej) => { await this.createFolder(); if (await this.exists()) { await this.delete(); } const writeStream = writeToPath(this.filePath, rows, { headers: true, includeEndRowDelimiter: true, transform: this.onWrite, }); writeStream.on('finish', res); writeStream.on('error', rej); }); } public async append(row: P | P[]): Promise { const rows = Array.isArray(row) ? row : [row]; return new Promise(async (res, rej) => { await this.createFolder(); const fileExists = await this.exists(); const writeStream = fs.createWriteStream(this.filePath, { encoding: 'utf-8', flags: 'a', }); const csvStream = format({ headers: !fileExists, includeEndRowDelimiter: true, }); csvStream.pipe(writeStream); rows.forEach((row) => csvStream.write(this.onWrite(row))); writeStream.on('finish', res); writeStream.on('error', rej); csvStream.end(); }); } } export class InMemoryStorage

implements IStorage

{ private value: P | null = null; async read(): Promise

{ return this.value; } async write(item: P): Promise { this.value = item; } } export class InMemoryListStorage

implements IListStorage

{ private list = new Set

(); async read(): Promise { return this.list.size === 0 ? null : Array.from(this.list); } async write(item: P[]): Promise { this.list = new Set(item); } async append(row: P[] | P): Promise { const rows = Array.isArray(row) ? row : [row]; rows.forEach((row) => this.list.add(row)); } }