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.
205 lines
7.5 KiB
205 lines
7.5 KiB
3 years ago
|
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
|
||
|
})
|
||
|
}
|
||
|
}
|