Skip to content

Commit

Permalink
Migrate notes to use new json setup (#3)
Browse files Browse the repository at this point in the history
* Fix tests

* Split json helpers into two functions

* Fix note ipcs to use new json helpers

* Less redundant code in json.ts

* Add tests for note ipcs
  • Loading branch information
EddieAbbondanzio authored Oct 22, 2022
1 parent f5b91f3 commit 288e4fc
Show file tree
Hide file tree
Showing 21 changed files with 519 additions and 400 deletions.
26 changes: 14 additions & 12 deletions src/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,21 @@ export function appIpcs(
p.join(config.content.dataDirectory, APP_STATE_PATH),
APP_STATE_SCHEMAS,
{
version: 1,
sidebar: {
scroll: 0,
sort: NoteSort.Alphanumeric,
width: DEFAULT_SIDEBAR_WIDTH,
defaultContent: {
version: 1,
sidebar: {
scroll: 0,
sort: NoteSort.Alphanumeric,
width: DEFAULT_SIDEBAR_WIDTH,
},
editor: {
isEditing: false,
scroll: 0,
tabs: [],
tabsScroll: 0,
},
focused: [],
},
editor: {
isEditing: false,
scroll: 0,
tabs: [],
tabsScroll: 0,
},
focused: [],
}
);
});
Expand Down
10 changes: 6 additions & 4 deletions src/main/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ export async function getConfig(): Promise<JsonFile<Config>> {
getConfigPath(),
CONFIG_SCHEMAS,
{
version: 2,
windowHeight: DEFAULT_WINDOW_HEIGHT,
windowWidth: DEFAULT_WINDOW_WIDTH,
logDirectory: app.getPath("logs"),
defaultContent: {
version: 2,
windowHeight: DEFAULT_WINDOW_HEIGHT,
windowWidth: DEFAULT_WINDOW_WIDTH,
logDirectory: app.getPath("logs"),
},
}
);

Expand Down
110 changes: 75 additions & 35 deletions src/main/json.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,101 @@
import { cloneDeep, last } from "lodash";
import { cloneDeep, isEqual, last } from "lodash";
import * as fsp from "fs/promises";
import * as fs from "fs";
import { ZodSchema } from "zod";
import { DeepPartial } from "tsdef";
import { deepUpdate } from "../shared/deepUpdate";

export interface JsonFile<T> {
content: Readonly<T>;
update(partial: DeepPartial<T>): Promise<void>;
export interface Versioned {
version: number;
}

export async function loadJsonFile<Content extends { version: number }>(
export interface JsonOptions<T> {
defaultContent?: T;
}

export async function writeJson<Content extends Versioned>(
filePath: string,
schemas: Record<number, ZodSchema>,
defaultContent: Content,
opts: { prettyPrint?: boolean } = { prettyPrint: true }
): Promise<JsonFile<Content>> {
content: Content
): Promise<void> {
const sortedSchemas = sortSchemas(schemas);
const [version, schema] = last(sortedSchemas)!;

if (content.version == null) {
content.version = version;
}

await schema.parseAsync(content);

const serialized = JSON.stringify(content, null, 2);
await fsp.writeFile(filePath, serialized, { encoding: "utf-8" });
}

export async function loadJson<Content extends Versioned>(
filePath: string,
schemas: Record<number, ZodSchema>,
opts?: JsonOptions<Content>
): Promise<Content> {
let originalContent: Content | undefined;
if (fs.existsSync(filePath)) {
const raw = await fsp.readFile(filePath, { encoding: "utf-8" });
originalContent = JSON.parse(raw);
}

// Apply default content if no content found, or if it had no versioning.
if (originalContent == null) {
originalContent = opts?.defaultContent;

if (originalContent == null) {
throw new Error(
`No json was found for ${filePath} and no default was provided.`
);
}
}

if (
originalContent == null ||
!originalContent.hasOwnProperty("version") ||
typeof originalContent.version !== "number"
) {
originalContent = defaultContent;
throw new Error(`No version found in json for ${filePath}`);
}

const { content, latestSchema, wasUpdated } = await runSchemas(
schemas,
originalContent
);
const { content, wasUpdated } = await runSchemas(schemas, originalContent);

// Save changes to file if any were made while migrating to latest.
if (wasUpdated) {
let jsonContent;
if (opts.prettyPrint) {
jsonContent = JSON.stringify(content, null, 2);
} else {
jsonContent = JSON.stringify(content);
}

const jsonContent = JSON.stringify(content, null, 2);
await fsp.writeFile(filePath, jsonContent, { encoding: "utf-8" });
}

return content;
}

export interface JsonFile<T> {
content: Readonly<T>;
update(partial: DeepPartial<T>): Promise<void>;
}

export async function loadJsonFile<Content extends Versioned>(
filePath: string,
schemas: Record<number, ZodSchema>,
opts?: JsonOptions<Content>
): Promise<JsonFile<Content>> {
const content = await loadJson(filePath, schemas, opts);
const latestSchema = schemas[content.version];

const update = async (partial: DeepPartial<Content>) => {
const updated = deepUpdate(content, partial);

// Validate against latest schema when saving to ensure we have valid content.
const validated = await latestSchema.parseAsync(updated);

let jsonString;
if (opts.prettyPrint) {
jsonString = JSON.stringify(updated, null, 2);
} else {
jsonString = JSON.stringify(updated);
// Don't write to file if no changes were made.
if (isEqual(content, validated)) {
return;
}

const jsonString = JSON.stringify(updated, null, 2);

fileHandler.content = validated;
await fsp.writeFile(filePath, jsonString, { encoding: "utf-8" });
};
Expand All @@ -72,17 +107,11 @@ export async function loadJsonFile<Content extends { version: number }>(
return fileHandler;
}

export async function runSchemas<Content extends { version: number }>(
export async function runSchemas<Content extends Versioned>(
schemas: Record<number, ZodSchema>,
content: Content
): Promise<{ content: Content; latestSchema: ZodSchema; wasUpdated: boolean }> {
const schemaArray = Object.entries(schemas)
.map<[number, ZodSchema]>(([version, schema]) => [
Number.parseInt(version, 10),
schema,
])
.sort(([a], [b]) => (a > b ? 1 : -1));

const schemaArray = sortSchemas(schemas);
if (schemaArray.length === 0) {
throw new Error(`Expected at least 1 schema in order to validate content.`);
}
Expand Down Expand Up @@ -110,3 +139,14 @@ export async function runSchemas<Content extends { version: number }>(
wasUpdated: validatedContent.version > content.version,
};
}

export function sortSchemas(
schemas: Record<number, ZodSchema>
): [number, ZodSchema][] {
return Object.entries(schemas)
.map<[number, ZodSchema]>(([version, schema]) => [
Number.parseInt(version, 10),
schema,
])
.sort(([a], [b]) => (a > b ? 1 : -1));
}
Loading

0 comments on commit 288e4fc

Please sign in to comment.