From fda2fb93920725729002575b94ff5adcfe71fbdc Mon Sep 17 00:00:00 2001 From: contributor Date: Tue, 11 Nov 2025 19:54:51 +0200 Subject: [PATCH] edited notes: recognize dateNote label value TODAY, MONTH, YEAR --- .../src/routes/api/edited-notes.spec.ts | 76 +++++++++++++++++++ apps/server/src/routes/api/edited-notes.ts | 53 ++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/routes/api/edited-notes.spec.ts diff --git a/apps/server/src/routes/api/edited-notes.spec.ts b/apps/server/src/routes/api/edited-notes.spec.ts new file mode 100644 index 000000000..29fd47e18 --- /dev/null +++ b/apps/server/src/routes/api/edited-notes.spec.ts @@ -0,0 +1,76 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import dayjs from "dayjs"; +import { resolveDateParams } from "./edited-notes.js"; + +function resolveAsDate(dateStr: string) { + return resolveDateParams(dateStr).date; +} + +describe("edited-notes::resolveAsDate", () => { + beforeEach(() => { + // Set a fixed date and time before each test + vi.useFakeTimers(); + vi.setSystemTime(new Date('2012-11-10T23:22:21Z')); // NOTE!!: Date wrap in my timezone + }); + + afterEach(() => { + // Restore real timers after each test + vi.useRealTimers(); + }); + + + it("resolves 'TODAY' to today's date", () => { + const expectedDate = dayjs().format("YYYY-MM-DD"); + const resolvedDate = resolveAsDate("TODAY"); + expect(resolvedDate).toBe(expectedDate); + }); + + it("resolves 'MONTH' to current month", () => { + const expectedMonth = dayjs().format("YYYY-MM"); + const resolvedMonth = resolveAsDate("MONTH"); + expect(resolvedMonth).toBe(expectedMonth); + }); + + it("resolves 'YEAR' to current year", () => { + const expectedYear = dayjs().format("YYYY"); + const resolvedYear = resolveAsDate("YEAR"); + expect(resolvedYear).toBe(expectedYear); + }); + + it("resolves 'TODAY-1' to yesterday's date", () => { + const expectedDate = dayjs().subtract(1, "day").format("YYYY-MM-DD"); + const resolvedDate = resolveAsDate("TODAY-1"); + expect(resolvedDate).toBe(expectedDate); + }); + + it("resolves 'MONTH-2' to 2 months ago", () => { + const expectedMonth = dayjs().subtract(2, "month").format("YYYY-MM"); + const resolvedMonth = resolveAsDate("MONTH-2"); + expect(resolvedMonth).toBe(expectedMonth); + }); + + it("resolves 'YEAR+1' to next year", () => { + const expectedYear = dayjs().add(1, "year").format("YYYY"); + const resolvedYear = resolveAsDate("YEAR+1"); + expect(resolvedYear).toBe(expectedYear); + }); + + it("returns original string for unrecognized keyword", () => { + const unrecognizedString = "NOT_A_DYNAMIC_DATE"; + const resolvedString = resolveAsDate(unrecognizedString); + expect(resolvedString).toBe(unrecognizedString); + }); + + it("returns original string for partially recognized keyword", () => { + const partialString = "TODAY-"; + const resolvedString = resolveAsDate(partialString); + expect(resolvedString).toBe(partialString); + }); + + it("resolves 'today' (lowercase) to today's date", () => { + const expectedDate = dayjs().format("YYYY-MM-DD"); + const resolvedDate = resolveAsDate("today"); + expect(resolvedDate).toBe(expectedDate); + }); + +}); diff --git a/apps/server/src/routes/api/edited-notes.ts b/apps/server/src/routes/api/edited-notes.ts index 953e5b4d0..22bd45158 100644 --- a/apps/server/src/routes/api/edited-notes.ts +++ b/apps/server/src/routes/api/edited-notes.ts @@ -6,6 +6,7 @@ import type { Request } from "express"; import { NotePojo } from "../../becca/becca-interface.js"; import type BNote from "../../becca/entities/bnote.js"; import { EditedNotesResponse } from "@triliumnext/commons"; +import dayjs from "dayjs"; interface NotePath { noteId: string; @@ -20,6 +21,9 @@ interface NotePojoWithNotePath extends NotePojo { } function getEditedNotesOnDate(req: Request) { + const resolvedDateParams = resolveDateParams(req.params.date); + + const sqlParams = { date: resolvedDateParams.date + "%" }; const noteIds = sql.getColumn(/*sql*/`\ SELECT notes.* @@ -35,7 +39,7 @@ function getEditedNotesOnDate(req: Request) { ) ORDER BY isDeleted LIMIT 50`, - { date: `${req.params.date}%` } + sqlParams ); let notes = becca.getNotes(noteIds, true); @@ -81,6 +85,53 @@ function getNotePathData(note: BNote): NotePath | undefined { } } +function formatDateFromKeywordAndDelta(keyword: string, delta: number): string { + const formatMap = new Map([ + ["today", { format: "YYYY-MM-DD", addUnit: "day" }], + ["month", { format: "YYYY-MM", addUnit: "month" }], + ["year", { format: "YYYY", addUnit: "year" }] + ]); + + const handler = formatMap.get(keyword); + + if (!handler) { + throw new Error(`Unrecognized keyword: ${keyword}`); + } + + const date = dayjs().add(delta, handler.addUnit); + return date.format(handler.format); +} + +interface DateValue { + // kind: "date", + date: string, +} + +type DateFilter = DateValue; + +/** + * Resolves date keyword with optional delta (e.g., "TODAY-1") to date + * @param dateStr date keyword (TODAY, MONTH, YEAR) or date in format YYYY-MM-DD (or beggining) + * @returns + */ +export function resolveDateParams(dateStr: string): DateFilter { + const match = dateStr.match(/^(today|month|year)([+-]\d+)?$/i); + + if (!match) { + return { + date: `${dateStr}` + } + } + + const keyword = match[1].toLowerCase(); + const delta = match[2] ? parseInt(match[2]) : 0; + + const date = formatDateFromKeywordAndDelta(keyword, delta); + return { + date: `${date}` + } +} + export default { getEditedNotesOnDate, };