|
|
|
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<string, unknown>
|
|
|
|
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<EmpathyGuide> {
|
|
|
|
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<void> {
|
|
|
|
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<Journal> {
|
|
|
|
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<void> {
|
|
|
|
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<TypesT extends AnyReferenceList>(
|
|
|
|
{autosaveNamespace, schemas}: { autosaveNamespace: string, schemas: TypesT }): Promise<{
|
|
|
|
validated: Partial<ReferencedTypes<TypesT>>,
|
|
|
|
unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, 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<ReferencedTypes<TypesT>> = {}
|
|
|
|
const unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, string>> = {}
|
|
|
|
schemas.forEach((schema: ReferencedSchema<TypesT>) => {
|
|
|
|
if (autosaved.hasOwnProperty(schema.key)) {
|
|
|
|
const value = autosaved[schema.key]
|
|
|
|
if (!schema.validate(value)) {
|
|
|
|
unvalidated[schema.key as keyof ReferencedTypes<TypesT>] = value
|
|
|
|
} else {
|
|
|
|
validated[schema.key as keyof ReferencedTypes<TypesT>] = value as Value<ReferencedSchema<TypesT>>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return {validated, unvalidated}
|
|
|
|
} finally {
|
|
|
|
await handle.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async saveAutosaveObject<T>({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<T, string, AnyReferenceList, ReferencedTypes<AnyReferenceList>>, value: T}): Promise<void> {
|
|
|
|
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<unknown, string, AnyReferenceList, ReferencedTypes<AnyReferenceList>>}): Promise<void> {
|
|
|
|
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<void> {
|
|
|
|
const path = this.getAutosaveFilePath(autosaveNamespace)
|
|
|
|
await rm(path, {
|
|
|
|
force: true
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|