Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions apps/desktop/src/store/tinybase/persister/calendar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { createMergeableStore } from "tinybase/with-schemas";
import { beforeEach, describe, expect, test, vi } from "vitest";

import { SCHEMA, type Schemas } from "@hypr/store";

import { createCalendarPersister } from "./calendar";

vi.mock("@hypr/plugin-path2", () => ({
commands: {
base: vi.fn().mockResolvedValue("/mock/data/dir/hyprnote"),
},
}));

vi.mock("@tauri-apps/plugin-fs", () => ({
mkdir: vi.fn().mockResolvedValue(undefined),
readTextFile: vi.fn(),
writeTextFile: vi.fn().mockResolvedValue(undefined),
}));

function createTestStore() {
return createMergeableStore()
.setTablesSchema(SCHEMA.table)
.setValuesSchema(SCHEMA.value);
}

describe("createCalendarPersister", () => {
let store: ReturnType<typeof createTestStore>;

beforeEach(() => {
store = createTestStore();
vi.clearAllMocks();
});

test("returns a persister object with expected methods", () => {
const persister = createCalendarPersister<Schemas>(store);

expect(persister).toBeDefined();
expect(persister.save).toBeTypeOf("function");
expect(persister.load).toBeTypeOf("function");
expect(persister.destroy).toBeTypeOf("function");
});

describe("load", () => {
test("loads calendars from json file", async () => {
const { readTextFile } = await import("@tauri-apps/plugin-fs");

const mockData = {
"cal-1": {
user_id: "user-1",
created_at: "2024-01-01T00:00:00Z",
tracking_id_calendar: "tracking-1",
name: "Work Calendar",
enabled: true,
provider: "apple",
source: "iCloud",
color: "#FF0000",
},
};
vi.mocked(readTextFile).mockResolvedValue(JSON.stringify(mockData));

const persister = createCalendarPersister<Schemas>(store);
await persister.load();

expect(readTextFile).toHaveBeenCalledWith(
"/mock/data/dir/hyprnote/calendars.json",
);
expect(store.getTable("calendars")).toEqual(mockData);
});

test("returns empty calendars when file does not exist", async () => {
const { readTextFile } = await import("@tauri-apps/plugin-fs");

vi.mocked(readTextFile).mockRejectedValue(
new Error("No such file or directory"),
);

const persister = createCalendarPersister<Schemas>(store);
await persister.load();

expect(store.getTable("calendars")).toEqual({});
});
});

describe("save", () => {
test("saves calendars to json file", async () => {
const { writeTextFile } = await import("@tauri-apps/plugin-fs");

store.setRow("calendars", "cal-1", {
user_id: "user-1",
created_at: "2024-01-01T00:00:00Z",
tracking_id_calendar: "tracking-1",
name: "Work Calendar",
enabled: true,
provider: "apple",
source: "iCloud",
color: "#FF0000",
});

const persister = createCalendarPersister<Schemas>(store);
await persister.save();

expect(writeTextFile).toHaveBeenCalledWith(
"/mock/data/dir/hyprnote/calendars.json",
expect.any(String),
);

const writtenContent = vi.mocked(writeTextFile).mock.calls[0][1];
const parsed = JSON.parse(writtenContent);

expect(parsed).toEqual({
"cal-1": {
user_id: "user-1",
created_at: "2024-01-01T00:00:00Z",
tracking_id_calendar: "tracking-1",
name: "Work Calendar",
enabled: true,
provider: "apple",
source: "iCloud",
color: "#FF0000",
},
});
});

test("writes empty object when no calendars exist", async () => {
const { writeTextFile } = await import("@tauri-apps/plugin-fs");

const persister = createCalendarPersister<Schemas>(store);
await persister.save();

expect(writeTextFile).toHaveBeenCalledWith(
"/mock/data/dir/hyprnote/calendars.json",
"{}",
);
});

test("saves multiple calendars", async () => {
const { writeTextFile } = await import("@tauri-apps/plugin-fs");

store.setRow("calendars", "cal-1", {
user_id: "user-1",
created_at: "2024-01-01T00:00:00Z",
tracking_id_calendar: "tracking-1",
name: "Work Calendar",
enabled: true,
provider: "apple",
source: "iCloud",
color: "#FF0000",
});

store.setRow("calendars", "cal-2", {
user_id: "user-1",
created_at: "2024-01-02T00:00:00Z",
tracking_id_calendar: "tracking-2",
name: "Personal Calendar",
enabled: false,
provider: "google",
source: "Gmail",
color: "#00FF00",
});

const persister = createCalendarPersister<Schemas>(store);
await persister.save();

const writtenContent = vi.mocked(writeTextFile).mock.calls[0][1];
const parsed = JSON.parse(writtenContent);

expect(Object.keys(parsed)).toHaveLength(2);
expect(parsed["cal-1"].name).toBe("Work Calendar");
expect(parsed["cal-2"].name).toBe("Personal Calendar");
});
});
});
15 changes: 15 additions & 0 deletions apps/desktop/src/store/tinybase/persister/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";

import { createSimpleJsonPersister, type PersisterMode } from "./utils";

export function createCalendarPersister<Schemas extends OptionalSchemas>(
store: MergeableStore<Schemas>,
config: { mode: PersisterMode } = { mode: "save-only" },
) {
return createSimpleJsonPersister(store, {
tableName: "calendars",
filename: "calendars.json",
label: "CalendarPersister",
mode: config.mode,
});
}
113 changes: 62 additions & 51 deletions apps/desktop/src/store/tinybase/persister/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";

import type { ChatMessageStorage } from "@hypr/store";

import { StoreOrMergeableStore } from "../store/shared";
import {
ensureDirsExist,
getChatDir,
getDataDir,
iterateTableRows,
type PersisterMode,
type TablesContent,
} from "./utils";

Expand Down Expand Up @@ -65,61 +67,70 @@ function collectMessagesByChatGroup(

export function createChatPersister<Schemas extends OptionalSchemas>(
store: MergeableStore<Schemas>,
config: { mode: PersisterMode } = { mode: "save-only" },
) {
const loadFn =
config.mode === "save-only" ? async () => undefined : async () => undefined;

const saveFn =
config.mode === "load-only"
? async () => {}
: async (getContent: () => unknown) => {
const [tables] = getContent() as [TablesContent | undefined, unknown];
const dataDir = await getDataDir();

const messagesByChatGroup = collectMessagesByChatGroup(tables);
if (messagesByChatGroup.size === 0) {
return;
}

const dirs = new Set<string>();
const writeOperations: Array<{ path: string; content: string }> = [];

for (const [
chatGroupId,
{ chatGroup, messages },
] of messagesByChatGroup) {
const chatDir = getChatDir(dataDir, chatGroupId);
dirs.add(chatDir);

const json: ChatJson = {
chat_group: chatGroup,
messages: messages.sort(
(a, b) =>
new Date(a.created_at || 0).getTime() -
new Date(b.created_at || 0).getTime(),
),
};
writeOperations.push({
path: `${chatDir}/_messages.json`,
content: JSON.stringify(json, null, 2),
});
}

try {
await ensureDirsExist(dirs);
} catch (e) {
console.error("Failed to ensure dirs exist:", e);
return;
}

for (const op of writeOperations) {
try {
await writeTextFile(op.path, op.content);
} catch (e) {
console.error(`Failed to write ${op.path}:`, e);
}
}
};

return createCustomPersister(
store,
async () => {
return undefined;
},
async (getContent) => {
const [tables] = getContent() as [TablesContent | undefined, unknown];
const dataDir = await getDataDir();

const messagesByChatGroup = collectMessagesByChatGroup(tables);
if (messagesByChatGroup.size === 0) {
return;
}

const dirs = new Set<string>();
const writeOperations: Array<{ path: string; content: string }> = [];

for (const [
chatGroupId,
{ chatGroup, messages },
] of messagesByChatGroup) {
const chatDir = getChatDir(dataDir, chatGroupId);
dirs.add(chatDir);

const json: ChatJson = {
chat_group: chatGroup,
messages: messages.sort(
(a, b) =>
new Date(a.created_at || 0).getTime() -
new Date(b.created_at || 0).getTime(),
),
};
writeOperations.push({
path: `${chatDir}/_messages.json`,
content: JSON.stringify(json, null, 2),
});
}

try {
await ensureDirsExist(dirs);
} catch (e) {
console.error("Failed to ensure dirs exist:", e);
return;
}

for (const op of writeOperations) {
try {
await writeTextFile(op.path, op.content);
} catch (e) {
console.error(`Failed to write ${op.path}:`, e);
}
}
},
loadFn,
saveFn,
(listener) => setInterval(listener, 1000),
(interval) => clearInterval(interval),
(error) => console.error("[ChatPersister]:", error),
StoreOrMergeableStore,
);
}
Loading
Loading