Mari's guided journal software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
mari-guided-journal/src/repository/LocalRepository.ts

205 lines
7.5 KiB

import {EmpathyGuide, EmpathyGuideJTD} from "../datatypes/EmpathyGuide";
import {Entry} from "../datatypes/Entry";
import {
ReferencedSchema,
ReferencedTypes,
schema,
SchemaData,
UntypedReferenceList
} from "../schemata/SchemaData";
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";
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 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()
}
}
async loadAutosaveObjects<TypesT extends UntypedReferenceList>(
{autosaveNamespace, schemas}: { autosaveNamespace: string, schemas: TypesT }): Promise<{
validated: Partial<ReferencedTypes<TypesT>>,
unvalidated: Partial<Record<keyof ReferencedTypes<TypesT>, unknown>>
}> {
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>, unknown>> = {}
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
}
}
})
return {validated, unvalidated}
} finally {
await handle.close()
}
}
async saveAutosaveObject<T>({schema, value, autosaveNamespace}: {autosaveNamespace: string, schema: SchemaData<T, any, any, any>, 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<any, any, any, any>}): 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
})
}
}