diff --git a/core/package.json b/core/package.json index ca9984bbd4..6cc773988c 100644 --- a/core/package.json +++ b/core/package.json @@ -26,6 +26,7 @@ "types": "build/src/index.d.ts", "dependencies": { "@codenamize/codenamize": "^1.1.1", + "@date-fns/utc": "^1.2.0", "@hapi/joi": "git+https://github.com/garden-io/joi.git#master", "@jsdevtools/readdir-enhanced": "^6.0.4", "@kubernetes/client-node": "^1.0.0-rc4", diff --git a/core/src/template-string/date-functions.ts b/core/src/template-string/date-functions.ts new file mode 100644 index 0000000000..6ddf122969 --- /dev/null +++ b/core/src/template-string/date-functions.ts @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018-2024 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import type { TemplateHelperFunction } from "./functions.js" +import { joi } from "../config/common.js" +import { format as formatFns, add, type Duration } from "date-fns" +import { UTCDateMini } from "@date-fns/utc" + +type ShiftDateTimeUnit = keyof Duration +const validShiftDateTimeUnits: ShiftDateTimeUnit[] = [ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", +] as const + +const validModifyDateTimeUnits = ["years", "months", "days", "hours", "minutes", "seconds", "milliseconds"] as const +type ModifyDateTimeUnit = (typeof validModifyDateTimeUnits)[number] +// This is still type-safe because every entry of ModifyDateTimeUnit must be declared in the index below. +const modifyDateFunctions: { [k in ModifyDateTimeUnit]: (date: Date, timeUnits: number) => void } = { + years: (date, timeUnits) => date.setUTCFullYear(timeUnits), + months: (date, timeUnits) => date.setUTCMonth(timeUnits), + days: (date, timeUnits) => date.setUTCDate(timeUnits), + hours: (date, timeUnits) => date.setUTCHours(timeUnits), + minutes: (date, timeUnits) => date.setUTCMinutes(timeUnits), + seconds: (date, timeUnits) => date.setUTCSeconds(timeUnits), + milliseconds: (date, timeUnits) => date.setUTCMilliseconds(timeUnits), +} as const + +const timeZoneComment = + "The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too." + +export const dateHelperFunctionSpecs: TemplateHelperFunction[] = [ + { + name: "formatDateUtc", + description: `Formats the given date using the specified format. ${timeZoneComment}`, + arguments: { + date: joi.string().required().description("The date to format."), + format: joi + .string() + .required() + .description("The format to use. See https://date-fns.org/v2.21.1/docs/format for details."), + }, + outputSchema: joi.string(), + exampleArguments: [ + { input: ["2021-01-01T00:00:00Z", "yyyy-MM-dd"], output: "2021-01-01" }, + { input: ["2021-01-01T00:00:00+0200", "yyyy-MM-dd"], output: "2020-12-31" }, + { input: ["2021-01-01T00:00:00Z", "yyyy-MM-dd HH:mm:ss"], output: "2021-01-01 00:00:00" }, + { input: ["2021-01-01T00:00:00+0200", "yyyy-MM-dd HH:mm:ss"], output: "2020-12-31 22:00:00" }, + ], + fn: (date: string, format: string) => { + const utcDate = new UTCDateMini(date) + return formatFns(utcDate, format) + }, + }, + { + name: "shiftDateUtc", + description: `Shifts the date by the specified amount of time units. ${timeZoneComment}`, + arguments: { + date: joi.string().required().description("The date to shift."), + amount: joi.number().required().description("The amount of time units to shift the date by."), + unit: joi + .string() + .valid(...validShiftDateTimeUnits) + .required() + .description("The time unit to shift the date by."), + }, + outputSchema: joi.string(), + exampleArguments: [ + { input: ["2021-01-01T00:00:00Z", 1, "seconds"], output: "2021-01-01T00:00:01.000Z" }, + { input: ["2021-01-01T00:00:00Z", -1, "seconds"], output: "2020-12-31T23:59:59.000Z" }, + { input: ["2021-01-01T00:00:00Z", 1, "minutes"], output: "2021-01-01T00:01:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", -1, "minutes"], output: "2020-12-31T23:59:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", 1, "hours"], output: "2021-01-01T01:00:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", -1, "hours"], output: "2020-12-31T23:00:00.000Z" }, + { input: ["2021-01-01T10:00:00+0200", 1, "hours"], output: "2021-01-01T09:00:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", 1, "days"], output: "2021-01-02T00:00:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", -1, "days"], output: "2020-12-31T00:00:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", 1, "months"], output: "2021-02-01T00:00:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", -1, "months"], output: "2020-12-01T00:00:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", 1, "years"], output: "2022-01-01T00:00:00.000Z" }, + { input: ["2021-01-01T00:00:00Z", -1, "years"], output: "2020-01-01T00:00:00.000Z" }, + ], + fn: (date: string, timeUnitAmount: number, unit: ShiftDateTimeUnit) => { + const dateClone = new Date(date) + return add(dateClone, { [unit]: timeUnitAmount }).toISOString() + }, + }, + { + name: "modifyDateUtc", + description: `Modifies the date by setting the specified amount of time units. ${timeZoneComment}`, + arguments: { + date: joi.string().required().description("The date to modify."), + amount: joi.number().required().description("The amount of time units to set."), + unit: joi + .string() + .valid(...validModifyDateTimeUnits) + .required() + .description("The time unit to set."), + }, + outputSchema: joi.string(), + exampleArguments: [ + { input: ["2021-01-01T00:00:00.234Z", 345, "milliseconds"], output: "2021-01-01T00:00:00.345Z" }, + { input: ["2021-01-01T00:00:05Z", 30, "seconds"], output: "2021-01-01T00:00:30.000Z" }, + { input: ["2021-01-01T00:01:00Z", 15, "minutes"], output: "2021-01-01T00:15:00.000Z" }, + { input: ["2021-01-01T12:00:00Z", 11, "hours"], output: "2021-01-01T11:00:00.000Z" }, + { input: ["2021-01-01T10:00:00+0200", 11, "hours"], output: "2021-01-01T11:00:00.000Z" }, + { input: ["2021-01-31T00:00:00Z", 1, "days"], output: "2021-01-01T00:00:00.000Z" }, + { input: ["2021-03-01T00:00:00Z", 0, "months"], output: "2021-01-01T00:00:00.000Z" }, // 0 (Jan) - 11 (Dec) + { input: ["2021-01-01T00:00:00Z", 2024, "years"], output: "2024-01-01T00:00:00.000Z" }, + ], + fn: (date: string, timeUnitAmount: number, unit: ModifyDateTimeUnit) => { + const dateClone = new Date(date) + const dateModifier = modifyDateFunctions[unit] + dateModifier(dateClone, timeUnitAmount) + return dateClone.toISOString() + }, + }, +] diff --git a/core/src/template-string/functions.ts b/core/src/template-string/functions.ts index 185df3efb3..645481ae2e 100644 --- a/core/src/template-string/functions.ts +++ b/core/src/template-string/functions.ts @@ -18,20 +18,21 @@ import { load, loadAll } from "js-yaml" import { safeDumpYaml } from "../util/serialization.js" import indentString from "indent-string" import { mayContainTemplateString } from "./template-string.js" +import { dateHelperFunctionSpecs } from "./date-functions.js" interface ExampleArgument { - input: any[] - output: any // Used to validate expected output + input: unknown[] + output: unknown // Used to validate expected output skipTest?: boolean } -interface TemplateHelperFunction { +export interface TemplateHelperFunction { name: string description: string arguments: { [name: string]: Joi.Schema } outputSchema: Joi.Schema exampleArguments: ExampleArgument[] - fn: Function + fn: (...args: any[]) => unknown } const helperFunctionSpecs: TemplateHelperFunction[] = [ @@ -407,6 +408,7 @@ const helperFunctionSpecs: TemplateHelperFunction[] = [ } }, }, + ...dateHelperFunctionSpecs, ] interface ResolvedHelperFunction extends TemplateHelperFunction { diff --git a/docs/reference/template-strings/functions.md b/docs/reference/template-strings/functions.md index 1543f14313..1f58cd9a26 100644 --- a/docs/reference/template-strings/functions.md +++ b/docs/reference/template-strings/functions.md @@ -51,6 +51,19 @@ Examples: * `${concat([1,2,3], [4,5])}` -> `[1,2,3,4,5]` * `${concat("string1", "string2")}` -> `"string1string2"` +## formatDateUtc + +Formats the given date using the specified format. The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too. + +Usage: `formatDateUtc(date, format)` + +Examples: + +* `${formatDateUtc("2021-01-01T00:00:00Z", "yyyy-MM-dd")}` -> `"2021-01-01"` +* `${formatDateUtc("2021-01-01T00:00:00+0200", "yyyy-MM-dd")}` -> `"2020-12-31"` +* `${formatDateUtc("2021-01-01T00:00:00Z", "yyyy-MM-dd HH:mm:ss")}` -> `"2021-01-01 00:00:00"` +* `${formatDateUtc("2021-01-01T00:00:00+0200", "yyyy-MM-dd HH:mm:ss")}` -> `"2020-12-31 22:00:00"` + ## indent Indents each line in the given string with the specified number of spaces. @@ -134,6 +147,23 @@ Examples: * `${lower("Some String")}` -> `"some string"` +## modifyDateUtc + +Modifies the date by setting the specified amount of time units. The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too. + +Usage: `modifyDateUtc(date, amount, unit)` + +Examples: + +* `${modifyDateUtc("2021-01-01T00:00:00.234Z", 345, "milliseconds")}` -> `"2021-01-01T00:00:00.345Z"` +* `${modifyDateUtc("2021-01-01T00:00:05Z", 30, "seconds")}` -> `"2021-01-01T00:00:30.000Z"` +* `${modifyDateUtc("2021-01-01T00:01:00Z", 15, "minutes")}` -> `"2021-01-01T00:15:00.000Z"` +* `${modifyDateUtc("2021-01-01T12:00:00Z", 11, "hours")}` -> `"2021-01-01T11:00:00.000Z"` +* `${modifyDateUtc("2021-01-01T10:00:00+0200", 11, "hours")}` -> `"2021-01-01T11:00:00.000Z"` +* `${modifyDateUtc("2021-01-31T00:00:00Z", 1, "days")}` -> `"2021-01-01T00:00:00.000Z"` +* `${modifyDateUtc("2021-03-01T00:00:00Z", 0, "months")}` -> `"2021-01-01T00:00:00.000Z"` +* `${modifyDateUtc("2021-01-01T00:00:00Z", 2024, "years")}` -> `"2024-01-01T00:00:00.000Z"` + ## replace Replaces all occurrences of a given substring in a string. @@ -155,6 +185,28 @@ Examples: * `${sha256("Some String")}` -> `"7f0fd64653ba0bb1a579ced2b6bf375e916cc60662109ee0c0b24f0a750c3a6c"` +## shiftDateUtc + +Shifts the date by the specified amount of time units. The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too. + +Usage: `shiftDateUtc(date, amount, unit)` + +Examples: + +* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "seconds")}` -> `"2021-01-01T00:00:01.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "seconds")}` -> `"2020-12-31T23:59:59.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "minutes")}` -> `"2021-01-01T00:01:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "minutes")}` -> `"2020-12-31T23:59:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "hours")}` -> `"2021-01-01T01:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "hours")}` -> `"2020-12-31T23:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T10:00:00+0200", 1, "hours")}` -> `"2021-01-01T09:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "days")}` -> `"2021-01-02T00:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "days")}` -> `"2020-12-31T00:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "months")}` -> `"2021-02-01T00:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "months")}` -> `"2020-12-01T00:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "years")}` -> `"2022-01-01T00:00:00.000Z"` +* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "years")}` -> `"2020-01-01T00:00:00.000Z"` + ## slice Slices a string or array at the specified start/end offsets. Note that you can use a negative number for the end offset to count backwards from the end. diff --git a/package-lock.json b/package-lock.json index da0fb67ee2..2f8a85354a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1257,6 +1257,7 @@ "license": "MPL-2.0", "dependencies": { "@codenamize/codenamize": "^1.1.1", + "@date-fns/utc": "^1.2.0", "@hapi/joi": "git+https://github.com/garden-io/joi.git#master", "@jsdevtools/readdir-enhanced": "^6.0.4", "@kubernetes/client-node": "^1.0.0-rc4", @@ -1504,6 +1505,11 @@ "version": "1.13.6", "license": "MIT" }, + "core/node_modules/@date-fns/utc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/utc/-/utc-1.2.0.tgz", + "integrity": "sha512-YLq+crMPJiBmIdkRmv9nZuZy1mVtMlDcUKlg4mvI0UsC/dZeIaGoGB5p/C4FrpeOhZ7zBTK03T58S0DFkRNMnw==" + }, "core/node_modules/@garden-io/platform-api-types": { "version": "1.914.0", "dev": true,