snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-23 17:39:41 +05:00
commit 906fb231cc
1544 changed files with 164131 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
{
"name": "@yaak/template-function-timestamp",
"displayName": "Timestamp Template Functions",
"version": "0.1.0",
"private": true,
"description": "Template functions for dealing with timestamps",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"test": "vp test --run tests"
},
"dependencies": {
"@date-fns/tz": "^1.4.1",
"date-fns": "^4.1.0"
}
}

View File

@@ -0,0 +1,187 @@
import type { PluginDefinition } from "@yaakapp/api";
import type { TemplateFunctionArg } from "@yaakapp-internal/plugins";
import type { ContextFn } from "date-fns";
import {
addDays,
addHours,
addMinutes,
addMonths,
addSeconds,
addYears,
format as formatDate,
isValid,
parseISO,
subDays,
subHours,
subMinutes,
subMonths,
subSeconds,
subYears,
} from "date-fns";
const dateArg: TemplateFunctionArg = {
type: "text",
name: "date",
label: "Timestamp",
optional: true,
description:
"Can be a timestamp in milliseconds, ISO string, or anything parseable by JS `new Date()`",
placeholder: new Date().toISOString(),
};
const expressionArg: TemplateFunctionArg = {
type: "text",
name: "expression",
label: "Expression",
description: "Modification expression (eg. '-5d +2h 3m'). Available units: y, M, d, h, m, s",
optional: true,
placeholder: "-5d +2h 3m",
};
const formatArg: TemplateFunctionArg = {
name: "format",
label: "Format String",
description:
"date-fns format string to describe the output (eg. \"yyyy-MM-dd 'at' HH:mm:ss\"). " +
"Wrap literal text in single quotes to escape it",
optional: true,
placeholder: "yyyy-MM-dd HH:mm:ss",
type: "text",
};
const formatDocsBanner: TemplateFunctionArg = {
type: "banner",
color: "info",
inputs: [
{
type: "markdown",
content:
"Uses [date-fns format tokens](https://date-fns.org/docs/format), " +
"not dayjs or Moment. Wrap literal text in single quotes to escape it.",
},
],
};
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: "timestamp.unix",
description: "Get the timestamp in seconds",
args: [dateArg],
onRender: async (_ctx, args) => {
const d = parseDateString(String(args.values.date ?? ""));
return String(Math.floor(d.getTime() / 1000));
},
},
{
name: "timestamp.unixMillis",
description: "Get the timestamp in milliseconds",
args: [dateArg],
onRender: async (_ctx, args) => {
const d = parseDateString(String(args.values.date ?? ""));
return String(d.getTime());
},
},
{
name: "timestamp.iso8601",
description: "Get the date in ISO8601 format",
args: [dateArg],
onRender: async (_ctx, args) => {
const d = parseDateString(String(args.values.date ?? ""));
return d.toISOString();
},
},
{
name: "timestamp.format",
description: "Format a date using a date-fns format string",
args: [formatDocsBanner, dateArg, formatArg],
previewArgs: [formatArg.name],
onRender: async (_ctx, args) => formatDatetime(args.values),
},
{
name: "timestamp.offset",
description: "Get the offset of a date based on an expression",
args: [dateArg, expressionArg],
previewArgs: [expressionArg.name],
onRender: async (_ctx, args) => calculateDatetime(args.values),
},
],
};
function applyDateOp(d: Date, sign: string, amount: number, unit: string): Date {
switch (unit) {
case "y":
return sign === "-" ? subYears(d, amount) : addYears(d, amount);
case "M":
return sign === "-" ? subMonths(d, amount) : addMonths(d, amount);
case "d":
return sign === "-" ? subDays(d, amount) : addDays(d, amount);
case "h":
return sign === "-" ? subHours(d, amount) : addHours(d, amount);
case "m":
return sign === "-" ? subMinutes(d, amount) : addMinutes(d, amount);
case "s":
return sign === "-" ? subSeconds(d, amount) : addSeconds(d, amount);
default:
throw new Error(`Invalid data calculation unit: ${unit}`);
}
}
function parseOp(op: string): { sign: string; amount: number; unit: string } | null {
const match = op.match(/^([+-]?)(\d+)([yMdhms])$/);
if (!match) {
throw new Error(`Invalid date expression: ${op}`);
}
const [, sign, amount, unit] = match;
if (!unit) return null;
return { sign: sign ?? "+", amount: Number(amount ?? 0), unit };
}
function parseDateString(date: string): Date {
if (!date.trim()) {
return new Date();
}
const isoDate = parseISO(date);
if (isValid(isoDate)) {
return isoDate;
}
const jsDate = /^\d+(\.\d+)?$/.test(date) ? new Date(Number(date)) : new Date(date);
if (isValid(jsDate)) {
return jsDate;
}
throw new Error(`Invalid date: ${date}`);
}
export function calculateDatetime(args: { date?: string; expression?: string }): string {
const { date, expression } = args;
let jsDate = parseDateString(date ?? "");
if (expression) {
const ops = String(expression)
.split(" ")
.map((s) => s.trim())
.filter(Boolean);
for (const op of ops) {
const parsed = parseOp(op);
if (parsed) {
jsDate = applyDateOp(jsDate, parsed.sign, parsed.amount, parsed.unit);
}
}
}
return jsDate.toISOString();
}
export function formatDatetime(args: {
date?: string;
format?: string;
in?: ContextFn<Date>;
}): string {
const { date, format } = args;
const d = parseDateString(date ?? "");
return formatDate(d, String(format || "yyyy-MM-dd HH:mm:ss"), { in: args.in });
}

View File

@@ -0,0 +1,72 @@
import { tz } from "@date-fns/tz";
import { describe, expect, it } from "vite-plus/test";
import { calculateDatetime, formatDatetime } from "../src";
describe("formatDatetime", () => {
it("returns formatted current date", () => {
const result = formatDatetime({});
expect(result).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
});
it("returns formatted specific date", () => {
const result = formatDatetime({ date: "2025-07-13T12:34:56" });
expect(result).toBe("2025-07-13 12:34:56");
});
it("returns formatted specific timestamp", () => {
const result = formatDatetime({ date: "1752435296000", in: tz("America/Vancouver") });
expect(result).toBe("2025-07-13 12:34:56");
});
it("returns formatted specific timestamp with decimals", () => {
const result = formatDatetime({ date: "1752435296000.19", in: tz("America/Vancouver") });
expect(result).toBe("2025-07-13 12:34:56");
});
it("returns formatted date with custom output", () => {
const result = formatDatetime({ date: "2025-07-13T12:34:56", format: "dd/MM/yyyy" });
expect(result).toBe("13/07/2025");
});
it("handles invalid date gracefully", () => {
expect(() => formatDatetime({ date: "invalid-date" })).toThrow("Invalid date: invalid-date");
});
});
describe("calculateDatetime", () => {
it("returns ISO string for current date", () => {
const result = calculateDatetime({});
expect(result).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
});
it("returns ISO string for specific date", () => {
const result = calculateDatetime({ date: "2025-07-13T12:34:56Z" });
expect(result).toBe("2025-07-13T12:34:56.000Z");
});
it("applies calc operations", () => {
const result = calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "+1d 2h" });
expect(result).toBe("2025-07-14T14:00:00.000Z");
});
it("applies negative calc operations", () => {
const result = calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "-1d -2h 1m" });
expect(result).toBe("2025-07-12T10:01:00.000Z");
});
it("throws error for invalid unit", () => {
expect(() => calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "+1x" })).toThrow(
"Invalid date expression: +1x",
);
});
it("throws error for invalid unit weird", () => {
expect(() => calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "+1&#^%" })).toThrow(
"Invalid date expression: +1&#^%",
);
});
it("throws error for bad expression", () => {
expect(() =>
calculateDatetime({ date: "2025-07-13T12:00:00Z", expression: "bad expr" }),
).toThrow("Invalid date expression: bad");
});
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}