import {EmpathyGuide, EmpathyGuideJTD} from "../datatypes/EmpathyGuide.js"; import {Entry} from "../datatypes/Entry.js"; import { ReferencedSchema, ReferencedTypes, schema, SchemaData, AnyReferenceList, Value } from "../schemata/SchemaData.js"; import {rm, open, FileHandle} from "fs/promises"; import {join, dirname} from "path"; import {dump, load} from "js-yaml"; import envPaths from "env-paths"; import makeDir from "make-dir"; import {Journal, JournalJTD} from "../datatypes/Journal.js"; export type AutosaveFile = Record export type AutosaveFileJTD = typeof AutosaveFileJTD export const AutosaveFileJTD = schema({ schema: { values: {} }, typeHint: null as AutosaveFile|null, key: "autosaveFile", references: [] }) export interface LocalRepositoryDependencies { rm: typeof rm open: typeof open makeDir: typeof makeDir } export class LocalRepository { private readonly paths: envPaths.Paths constructor() { this.paths = envPaths("guided-journal") } private getJournalFilePath(): string { return join(this.paths.data, "journal.yaml") } private getEmpathyGuideFilePath(): string { return join(this.paths.config, "empathyGuide.yaml") } private getAutosaveFilePath(autosaveNamespace: string): string { return join(this.paths.data, "autosave", `${autosaveNamespace}.yaml`) } async loadEmpathyGuide(): Promise { const path = this.getEmpathyGuideFilePath() let handle: FileHandle try { handle = await open(path, "a+") } catch (e) { if (typeof e === "object" && e.hasOwnProperty("code") && typeof e.code === "string" && e.code === "ENOENT") { return {} // no empathy guide exists, so return a blank one } throw e } try { const fileYaml = await handle.readFile({encoding: "utf-8"}) const saved = load(fileYaml) if (!EmpathyGuideJTD.validate(saved)) { throw Error(`Empathy guide file was corrupted`) } return saved } finally { await handle.close() } } async saveEmpathyGuide(empathyGuide: EmpathyGuide): Promise { const path = this.getEmpathyGuideFilePath() await makeDir(dirname(path)) const handle = await open(path, "w") try { await handle.writeFile(dump(empathyGuide), {encoding: "utf-8"}) } finally { await handle.close() } } async getEntries(): Promise { const path = this.getJournalFilePath() let handle: FileHandle try { handle = await open(path, "r") } catch (e) { if (typeof e === "object" && e.hasOwnProperty("code") && typeof e.code === "string" && e.code === "ENOENT") { // file does not exist :( return {entries: []} // no entries saved } throw e } try { const fileYaml = await handle.readFile({encoding: "utf-8"}) const saved = load(fileYaml) if (!JournalJTD.validate(saved)) { throw Error(`Journal file was corrupted`) } return saved } finally { await handle.close() } } async saveEntry(entry: Entry): Promise { const path = this.getJournalFilePath() await makeDir(dirname(path)) const handle = await open(path, "a+") try { const fileYaml = await handle.readFile({encoding: "utf-8"}) const original = fileYaml !== "" ? load(fileYaml) : {} if (!JournalJTD.validate(original)) { throw Error(`Journal file was corrupted`) } const appended = {entries: [...original.entries || [], entry]} await handle.truncate(0) await handle.writeFile(dump(appended), {encoding: "utf-8"}) } finally { await handle.close() } } // TODO: Enable autosave for all commands async loadAutosaveObjects( {autosaveNamespace, schemas}: { autosaveNamespace: string, schemas: TypesT }): Promise<{ validated: Partial>, unvalidated: Partial, string>> }> { const path = this.getAutosaveFilePath(autosaveNamespace) let handle: FileHandle try { handle = await open(path, "r") } catch (e) { if (typeof e === "object" && e.hasOwnProperty("code") && typeof e.code === "string" && e.code === "ENOENT") { // file does not exist :( return {validated: {}, unvalidated: {}} // nothing was autosaved } throw e } try { const fileYaml = await handle.readFile({encoding: "utf-8"}) const autosaved = load(fileYaml) if (!AutosaveFileJTD.validate(autosaved)) { throw Error(`Autosave file ${autosaveNamespace} was too corrupted to make sense of`) } const validated: Partial> = {} const unvalidated: Partial, string>> = {} schemas.forEach((schema: ReferencedSchema) => { if (autosaved.hasOwnProperty(schema.key)) { const value = autosaved[schema.key] if (!schema.validate(value)) { unvalidated[schema.key as keyof ReferencedTypes] = value } else { validated[schema.key as keyof ReferencedTypes] = value as Value> } } }) return {validated, unvalidated} } finally { await handle.close() } } async saveAutosaveObject({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData>, value: T}): Promise { const path = this.getAutosaveFilePath(autosaveNamespace) await makeDir(dirname(path)) const handle = await open(path, "a+") try { const fileYaml = await handle.readFile({encoding: "utf-8"}) const original = load(fileYaml) if (!AutosaveFileJTD.validate(original)) { throw Error(`Autosave file ${autosaveNamespace} was corrupted`) } original[schema.key] = value await handle.truncate(0) await handle.writeFile(dump(original), {encoding: "utf-8"}) } finally { await handle.close() } } async clearAutosaveObject({schema, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData>}): Promise { const path = this.getAutosaveFilePath(autosaveNamespace) await makeDir(dirname(path)) const handle = await open(path, "a+") try { const fileYaml = await handle.readFile({encoding: "utf-8"}) const original = load(fileYaml) if (!AutosaveFileJTD.validate(original)) { throw Error(`Autosave file ${autosaveNamespace} was corrupted`) } delete original[schema.key] await handle.truncate(0) await handle.writeFile(dump(original), {encoding: "utf-8"}) } finally { await handle.close() } } async clearAutosaveObjects(autosaveNamespace: string): Promise { const path = this.getAutosaveFilePath(autosaveNamespace) await rm(path, { force: true }) } }