From cd74f859f4f00ced471b12cfc4b51a5bef8f74e2 Mon Sep 17 00:00:00 2001 From: Ed Abbondanzio Date: Sun, 18 Sep 2022 17:14:15 -0400 Subject: [PATCH] Add support for deserializing string dates --- src/main/app.ts | 12 ++---------- .../migrations/appState/1_initialDefinition.ts | 4 +++- src/shared/domain/index.ts | 5 +++++ src/shared/domain/note.ts | 7 ++++--- test/shared/domain/index.spec.ts | 18 ++++++++++++++++-- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/main/app.ts b/src/main/app.ts index ab7caecb..015998c2 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -20,6 +20,7 @@ import { Config } from "../shared/domain/config"; import { APP_STATE_MIGRATIONS } from "./migrations/appState"; import p from "path"; import { MissingDataDirectoryError } from "../shared/errors"; +import { DATE_OR_STRING_SCHEMA } from "../shared/domain"; export const APP_STATE_PATH = "ui.json"; @@ -53,16 +54,7 @@ export const APP_STATE_SCHEMA = z z.object({ noteId: z.string(), // Intentionally omitted noteContent - lastActive: z.preprocess((arg) => { - // If loaded from JSON dates will be a string - if (typeof arg === "string") { - return parseJSON(arg); - } - // But if it was already in memory they'll be a date. - else if (arg instanceof Date) { - return arg; - } - }, z.date().optional()), + lastActive: DATE_OR_STRING_SCHEMA.optional(), }) ) .default([]), diff --git a/src/main/migrations/appState/1_initialDefinition.ts b/src/main/migrations/appState/1_initialDefinition.ts index 05cef09a..372911b0 100644 --- a/src/main/migrations/appState/1_initialDefinition.ts +++ b/src/main/migrations/appState/1_initialDefinition.ts @@ -1,4 +1,6 @@ +import { parseJSON } from "date-fns"; import { z } from "zod"; +import { DATE_OR_STRING_SCHEMA } from "../../../shared/domain"; import { DEFAULT_NOTE_SORTING_ALGORITHM, NoteSort, @@ -30,7 +32,7 @@ export const appStateSchemaV1 = z.object({ z.object({ noteId: z.string(), // Intentionally omitted noteContent - lastActive: z.date().optional(), + lastActive: DATE_OR_STRING_SCHEMA.optional(), }) ), tabsScroll: z.number().default(0), diff --git a/src/shared/domain/index.ts b/src/shared/domain/index.ts index 630debdb..8df2f162 100644 --- a/src/shared/domain/index.ts +++ b/src/shared/domain/index.ts @@ -1,3 +1,4 @@ +import { parseJSON } from "date-fns"; import { customAlphabet } from "nanoid"; import { z } from "zod"; @@ -9,6 +10,10 @@ export const uuid = customAlphabet(ID_ALPHABET, ID_LENGTH); export const UUID_REGEX = /[a-zA-Z0-9]{10}$/; export const UUID_SCHEMA = z.string().refine((val) => UUID_REGEX.test(val)); +export const DATE_OR_STRING_SCHEMA = z.union([ + z.date(), + z.string().transform((v) => parseJSON(v)), +]); export interface Resource { id: string; diff --git a/src/shared/domain/note.ts b/src/shared/domain/note.ts index faa5bb21..f22e5330 100644 --- a/src/shared/domain/note.ts +++ b/src/shared/domain/note.ts @@ -1,7 +1,8 @@ -import { UUID_SCHEMA, Resource, uuid } from "."; +import { UUID_SCHEMA, Resource, uuid, DATE_OR_STRING_SCHEMA } from "."; import { isBlank } from "../utils"; import { isEmpty, orderBy } from "lodash"; import { z } from "zod"; +import { parseJSON } from "date-fns"; export interface Note extends Resource { name: string; @@ -113,8 +114,8 @@ export const NOTE_SCHEMA = z.object({ .min(1, "Name must be at least 1 char long") .max(64, "Name must be 64 chars or less."), flags: z.number().optional(), - dateCreated: z.date(), - dateUpdated: z.date().optional(), + dateCreated: DATE_OR_STRING_SCHEMA, + dateUpdated: DATE_OR_STRING_SCHEMA.optional(), sort: z.nativeEnum(NoteSort).optional(), }); diff --git a/test/shared/domain/index.spec.ts b/test/shared/domain/index.spec.ts index a5753f1f..1fb52ea9 100644 --- a/test/shared/domain/index.spec.ts +++ b/test/shared/domain/index.spec.ts @@ -1,6 +1,20 @@ -import { UUID_SCHEMA } from "../../../src/shared/domain"; +import { isEqual, parseJSON } from "date-fns"; +import { DATE_OR_STRING_SCHEMA, UUID_SCHEMA } from "../../../src/shared/domain"; -test("uuidSchema", async () => { +test("UUID_SCHEMA", async () => { const uuid = "d3ZU8GmTG3"; expect(await UUID_SCHEMA.parseAsync(uuid)).toBe("d3ZU8GmTG3"); }); + +test("DATE_OR_STRING_SCHEMA", async () => { + // In the past we've had some deserialize bugs pop up because JSON stores dates + // as strings. This test is just to help prevent regressions. + + const date = new Date("2020-01-01T20:37:21.765Z") + const parsed = await DATE_OR_STRING_SCHEMA.parseAsync(date); + expect(isEqual(date, parsed)).toBe(true); + + const serializedDate = "2022-09-09T20:37:21.765Z"; + const parsedSerializedDate = parseJSON(serializedDate); + expect(isEqual(parsedSerializedDate, parsedSerializedDate)).toBe(true); +}