@schafevormfenster/rest-commons 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CONTRIBUTING.md +1190 -0
  2. package/README.md +275 -0
  3. package/bin/setup.js +10 -0
  4. package/dist/api-schemas/error.schema.d.ts +20 -0
  5. package/dist/api-schemas/error.schema.d.ts.map +1 -0
  6. package/dist/api-schemas/error.schema.js +17 -0
  7. package/dist/api-schemas/health.schema.d.ts +497 -0
  8. package/dist/api-schemas/health.schema.d.ts.map +1 -0
  9. package/dist/api-schemas/health.schema.js +33 -0
  10. package/dist/api-schemas/okay.schema.d.ts +13 -0
  11. package/dist/api-schemas/okay.schema.d.ts.map +1 -0
  12. package/dist/api-schemas/okay.schema.js +5 -0
  13. package/dist/api-schemas/paginated-results.schema.d.ts +59 -0
  14. package/dist/api-schemas/paginated-results.schema.d.ts.map +1 -0
  15. package/dist/api-schemas/paginated-results.schema.js +10 -0
  16. package/dist/api-schemas/partial-results.schema.d.ts +30 -0
  17. package/dist/api-schemas/partial-results.schema.d.ts.map +1 -0
  18. package/dist/api-schemas/partial-results.schema.js +10 -0
  19. package/dist/api-schemas/result.schema.d.ts +17 -0
  20. package/dist/api-schemas/result.schema.d.ts.map +1 -0
  21. package/dist/api-schemas/result.schema.js +5 -0
  22. package/dist/api-schemas/results.schema.d.ts +21 -0
  23. package/dist/api-schemas/results.schema.d.ts.map +1 -0
  24. package/dist/api-schemas/results.schema.js +5 -0
  25. package/dist/helpers/correlation/get-correlation-id.d.ts +7 -0
  26. package/dist/helpers/correlation/get-correlation-id.d.ts.map +1 -0
  27. package/dist/helpers/correlation/get-correlation-id.js +16 -0
  28. package/dist/helpers/correlation/get-header.d.ts +7 -0
  29. package/dist/helpers/correlation/get-header.d.ts.map +1 -0
  30. package/dist/helpers/correlation/get-header.js +11 -0
  31. package/dist/helpers/detect-mime-type.d.ts +11 -0
  32. package/dist/helpers/detect-mime-type.d.ts.map +1 -0
  33. package/dist/helpers/detect-mime-type.js +40 -0
  34. package/dist/helpers/detect-suspicious-patterns.d.ts +8 -0
  35. package/dist/helpers/detect-suspicious-patterns.d.ts.map +1 -0
  36. package/dist/helpers/detect-suspicious-patterns.js +55 -0
  37. package/dist/helpers/eventify-constants.types.d.ts +32 -0
  38. package/dist/helpers/eventify-constants.types.d.ts.map +1 -0
  39. package/dist/helpers/eventify-constants.types.js +40 -0
  40. package/dist/helpers/hash-binary.d.ts +21 -0
  41. package/dist/helpers/hash-binary.d.ts.map +1 -0
  42. package/dist/helpers/hash-binary.js +28 -0
  43. package/dist/helpers/mime-types/detect-image-mime-type.d.ts +5 -0
  44. package/dist/helpers/mime-types/detect-image-mime-type.d.ts.map +1 -0
  45. package/dist/helpers/mime-types/detect-image-mime-type.js +41 -0
  46. package/dist/helpers/mime-types/detect-ole-mime-type.d.ts +6 -0
  47. package/dist/helpers/mime-types/detect-ole-mime-type.d.ts.map +1 -0
  48. package/dist/helpers/mime-types/detect-ole-mime-type.js +34 -0
  49. package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts +5 -0
  50. package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts.map +1 -0
  51. package/dist/helpers/mime-types/detect-pdf-mime-type.js +13 -0
  52. package/dist/helpers/mime-types/detect-zip-mime-type.d.ts +6 -0
  53. package/dist/helpers/mime-types/detect-zip-mime-type.d.ts.map +1 -0
  54. package/dist/helpers/mime-types/detect-zip-mime-type.js +23 -0
  55. package/dist/helpers/parameter-validation.d.ts +6 -0
  56. package/dist/helpers/parameter-validation.d.ts.map +1 -0
  57. package/dist/helpers/parameter-validation.js +19 -0
  58. package/dist/helpers/parameter-validation.types.d.ts +16 -0
  59. package/dist/helpers/parameter-validation.types.d.ts.map +1 -0
  60. package/dist/helpers/parameter-validation.types.js +38 -0
  61. package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts +6 -0
  62. package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts.map +1 -0
  63. package/dist/helpers/response-headers/build-api-unauthorized-headers.js +23 -0
  64. package/dist/helpers/response-headers/environment.types.d.ts +2 -0
  65. package/dist/helpers/response-headers/environment.types.d.ts.map +1 -0
  66. package/dist/helpers/response-headers/environment.types.js +1 -0
  67. package/dist/helpers/response-headers/resolve-environment.d.ts +8 -0
  68. package/dist/helpers/response-headers/resolve-environment.d.ts.map +1 -0
  69. package/dist/helpers/response-headers/resolve-environment.js +18 -0
  70. package/dist/helpers/slugify.d.ts +15 -0
  71. package/dist/helpers/slugify.d.ts.map +1 -0
  72. package/dist/helpers/slugify.js +32 -0
  73. package/dist/index.d.ts +36 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +41 -0
  76. package/dist/normalization/normalize-list.d.ts +11 -0
  77. package/dist/normalization/normalize-list.d.ts.map +1 -0
  78. package/dist/normalization/normalize-list.js +19 -0
  79. package/dist/normalization/normalize-location.d.ts +16 -0
  80. package/dist/normalization/normalize-location.d.ts.map +1 -0
  81. package/dist/normalization/normalize-location.js +26 -0
  82. package/dist/primitives/coordinate-precision.d.ts +10 -0
  83. package/dist/primitives/coordinate-precision.d.ts.map +1 -0
  84. package/dist/primitives/coordinate-precision.js +27 -0
  85. package/dist/primitives/geo-point.schema.d.ts +8 -0
  86. package/dist/primitives/geo-point.schema.d.ts.map +1 -0
  87. package/dist/primitives/geo-point.schema.js +10 -0
  88. package/dist/primitives/geoname-id.schema.d.ts +8 -0
  89. package/dist/primitives/geoname-id.schema.d.ts.map +1 -0
  90. package/dist/primitives/geoname-id.schema.js +9 -0
  91. package/dist/primitives/international-zip.schema.d.ts +76 -0
  92. package/dist/primitives/international-zip.schema.d.ts.map +1 -0
  93. package/dist/primitives/international-zip.schema.js +81 -0
  94. package/dist/primitives/latitude.schema.d.ts +9 -0
  95. package/dist/primitives/latitude.schema.d.ts.map +1 -0
  96. package/dist/primitives/latitude.schema.js +13 -0
  97. package/dist/primitives/location.schema.d.ts +8 -0
  98. package/dist/primitives/location.schema.d.ts.map +1 -0
  99. package/dist/primitives/location.schema.js +15 -0
  100. package/dist/primitives/longitude.schema.d.ts +9 -0
  101. package/dist/primitives/longitude.schema.d.ts.map +1 -0
  102. package/dist/primitives/longitude.schema.js +13 -0
  103. package/dist/primitives/numeric-id.schema.d.ts +8 -0
  104. package/dist/primitives/numeric-id.schema.d.ts.map +1 -0
  105. package/dist/primitives/numeric-id.schema.js +10 -0
  106. package/dist/primitives/slug.schema.d.ts +17 -0
  107. package/dist/primitives/slug.schema.d.ts.map +1 -0
  108. package/dist/primitives/slug.schema.js +30 -0
  109. package/dist/primitives/uuid.schema.d.ts +8 -0
  110. package/dist/primitives/uuid.schema.d.ts.map +1 -0
  111. package/dist/primitives/uuid.schema.js +9 -0
  112. package/dist/primitives/wikidata-id.schema.d.ts +9 -0
  113. package/dist/primitives/wikidata-id.schema.d.ts.map +1 -0
  114. package/dist/primitives/wikidata-id.schema.js +10 -0
  115. package/dist/time/boundary-enforcement.d.ts +11 -0
  116. package/dist/time/boundary-enforcement.d.ts.map +1 -0
  117. package/dist/time/boundary-enforcement.js +43 -0
  118. package/dist/time/bounded-time.schema.d.ts +31 -0
  119. package/dist/time/bounded-time.schema.d.ts.map +1 -0
  120. package/dist/time/bounded-time.schema.js +77 -0
  121. package/dist/time/flexible-time-parser.d.ts +12 -0
  122. package/dist/time/flexible-time-parser.d.ts.map +1 -0
  123. package/dist/time/flexible-time-parser.js +94 -0
  124. package/dist/time/flexible-time.schema.d.ts +31 -0
  125. package/dist/time/flexible-time.schema.d.ts.map +1 -0
  126. package/dist/time/flexible-time.schema.js +31 -0
  127. package/dist/time/get-week-end.d.ts +10 -0
  128. package/dist/time/get-week-end.d.ts.map +1 -0
  129. package/dist/time/get-week-end.js +25 -0
  130. package/dist/time/get-week-start.d.ts +10 -0
  131. package/dist/time/get-week-start.d.ts.map +1 -0
  132. package/dist/time/get-week-start.js +25 -0
  133. package/dist/time/is-relative-time.d.ts +8 -0
  134. package/dist/time/is-relative-time.d.ts.map +1 -0
  135. package/dist/time/is-relative-time.js +9 -0
  136. package/dist/time/iso8601.schema.d.ts +14 -0
  137. package/dist/time/iso8601.schema.d.ts.map +1 -0
  138. package/dist/time/iso8601.schema.js +17 -0
  139. package/dist/time/iso8601.types.d.ts +6 -0
  140. package/dist/time/iso8601.types.d.ts.map +1 -0
  141. package/dist/time/iso8601.types.js +11 -0
  142. package/dist/time/parse-relative-time.d.ts +9 -0
  143. package/dist/time/parse-relative-time.d.ts.map +1 -0
  144. package/dist/time/parse-relative-time.js +36 -0
  145. package/dist/time/relative-time.schema.d.ts +23 -0
  146. package/dist/time/relative-time.schema.d.ts.map +1 -0
  147. package/dist/time/relative-time.schema.js +25 -0
  148. package/dist/time/since-parameter.schema.d.ts +8 -0
  149. package/dist/time/since-parameter.schema.d.ts.map +1 -0
  150. package/dist/time/since-parameter.schema.js +56 -0
  151. package/dist/time/time-helpers.d.ts +19 -0
  152. package/dist/time/time-helpers.d.ts.map +1 -0
  153. package/dist/time/time-helpers.js +56 -0
  154. package/dist/time/time-schemas.d.ts +20 -0
  155. package/dist/time/time-schemas.d.ts.map +1 -0
  156. package/dist/time/time-schemas.js +25 -0
  157. package/dist/time/timezone.types.d.ts +17 -0
  158. package/dist/time/timezone.types.d.ts.map +1 -0
  159. package/dist/time/timezone.types.js +15 -0
  160. package/dist/validation/zod-error-handler.d.ts +3 -0
  161. package/dist/validation/zod-error-handler.d.ts.map +1 -0
  162. package/dist/validation/zod-error-handler.js +189 -0
  163. package/dist/validation/zod-utils.d.ts +9 -0
  164. package/dist/validation/zod-utils.d.ts.map +1 -0
  165. package/dist/validation/zod-utils.js +23 -0
  166. package/eslint.config.mjs +16 -0
  167. package/package.json +44 -0
  168. package/src/api-schemas/error.schema.test.ts +27 -0
  169. package/src/api-schemas/error.schema.ts +23 -0
  170. package/src/api-schemas/health.schema.test.ts +104 -0
  171. package/src/api-schemas/health.schema.ts +63 -0
  172. package/src/api-schemas/okay.schema.test.ts +15 -0
  173. package/src/api-schemas/okay.schema.ts +8 -0
  174. package/src/api-schemas/paginated-results.schema.ts +17 -0
  175. package/src/api-schemas/partial-results.schema.ts +13 -0
  176. package/src/api-schemas/result.schema.test.ts +19 -0
  177. package/src/api-schemas/result.schema.ts +9 -0
  178. package/src/api-schemas/results.schema.test.ts +15 -0
  179. package/src/api-schemas/results.schema.ts +9 -0
  180. package/src/helpers/correlation/get-correlation-id.test.ts +126 -0
  181. package/src/helpers/correlation/get-correlation-id.ts +22 -0
  182. package/src/helpers/correlation/get-header.test.ts +179 -0
  183. package/src/helpers/correlation/get-header.ts +21 -0
  184. package/src/helpers/detect-mime-type.test.ts +100 -0
  185. package/src/helpers/detect-mime-type.ts +46 -0
  186. package/src/helpers/detect-suspicious-patterns.test.ts +45 -0
  187. package/src/helpers/detect-suspicious-patterns.ts +57 -0
  188. package/src/helpers/eventify-constants.test.ts +52 -0
  189. package/src/helpers/eventify-constants.types.test.ts +52 -0
  190. package/src/helpers/eventify-constants.types.ts +51 -0
  191. package/src/helpers/hash-binary.test.ts +60 -0
  192. package/src/helpers/hash-binary.ts +30 -0
  193. package/src/helpers/mime-types/detect-image-mime-type.test.ts +73 -0
  194. package/src/helpers/mime-types/detect-image-mime-type.ts +50 -0
  195. package/src/helpers/mime-types/detect-ole-mime-type.test.ts +86 -0
  196. package/src/helpers/mime-types/detect-ole-mime-type.ts +44 -0
  197. package/src/helpers/mime-types/detect-pdf-mime-type.test.ts +39 -0
  198. package/src/helpers/mime-types/detect-pdf-mime-type.ts +15 -0
  199. package/src/helpers/mime-types/detect-zip-mime-type.test.ts +88 -0
  200. package/src/helpers/mime-types/detect-zip-mime-type.ts +28 -0
  201. package/src/helpers/parameter-validation.test.ts +35 -0
  202. package/src/helpers/parameter-validation.ts +32 -0
  203. package/src/helpers/process-eventify-request.ts +146 -0
  204. package/src/helpers/response-headers/build-api-unauthorized-headers.ts +30 -0
  205. package/src/helpers/response-headers/environment.types.ts +1 -0
  206. package/src/helpers/response-headers/resolve-environment.ts +17 -0
  207. package/src/helpers/slugify.test.ts +77 -0
  208. package/src/helpers/slugify.ts +34 -0
  209. package/src/index.ts +46 -0
  210. package/src/normalization/normalize-list.test.ts +43 -0
  211. package/src/normalization/normalize-list.ts +21 -0
  212. package/src/normalization/normalize-location.test.ts +91 -0
  213. package/src/normalization/normalize-location.ts +29 -0
  214. package/src/primitives/coordinate-precision.test.ts +46 -0
  215. package/src/primitives/coordinate-precision.ts +30 -0
  216. package/src/primitives/geo-point.schema.test.ts +70 -0
  217. package/src/primitives/geo-point.schema.ts +14 -0
  218. package/src/primitives/geoname-id.schema.test.ts +60 -0
  219. package/src/primitives/geoname-id.schema.ts +12 -0
  220. package/src/primitives/international-zip.schema.test.ts +212 -0
  221. package/src/primitives/international-zip.schema.ts +103 -0
  222. package/src/primitives/latitude.schema.test.ts +77 -0
  223. package/src/primitives/latitude.schema.ts +20 -0
  224. package/src/primitives/location.schema.test.ts +21 -0
  225. package/src/primitives/location.schema.ts +22 -0
  226. package/src/primitives/longitude.schema.test.ts +77 -0
  227. package/src/primitives/longitude.schema.ts +20 -0
  228. package/src/primitives/numeric-id.schema.test.ts +32 -0
  229. package/src/primitives/numeric-id.schema.ts +13 -0
  230. package/src/primitives/slug.schema.test.ts +101 -0
  231. package/src/primitives/slug.schema.ts +41 -0
  232. package/src/primitives/uuid.schema.test.ts +45 -0
  233. package/src/primitives/uuid.schema.ts +12 -0
  234. package/src/primitives/wikidata-id.schema.test.ts +51 -0
  235. package/src/primitives/wikidata-id.schema.ts +16 -0
  236. package/src/time/README.md +220 -0
  237. package/src/time/boundary-enforcement.test.ts +130 -0
  238. package/src/time/boundary-enforcement.ts +59 -0
  239. package/src/time/bounded-time.schema.test.ts +294 -0
  240. package/src/time/bounded-time.schema.ts +111 -0
  241. package/src/time/flexible-time-parser.test.ts +586 -0
  242. package/src/time/flexible-time-parser.ts +122 -0
  243. package/src/time/flexible-time.schema.test.ts +243 -0
  244. package/src/time/flexible-time.schema.ts +43 -0
  245. package/src/time/is-relative-time.test.ts +23 -0
  246. package/src/time/is-relative-time.ts +9 -0
  247. package/src/time/iso8601.schema.ts +29 -0
  248. package/src/time/iso8601.types.test.ts +112 -0
  249. package/src/time/iso8601.types.ts +21 -0
  250. package/src/time/parse-relative-time.test.ts +49 -0
  251. package/src/time/parse-relative-time.ts +50 -0
  252. package/src/time/relative-time.schema.test.ts +23 -0
  253. package/src/time/relative-time.schema.ts +38 -0
  254. package/src/time/since-parameter.schema.test.ts +59 -0
  255. package/src/time/since-parameter.schema.ts +69 -0
  256. package/src/time/time-helpers.test.ts +263 -0
  257. package/src/time/time-helpers.ts +78 -0
  258. package/src/time/time-schemas.test.ts +181 -0
  259. package/src/time/time-schemas.ts +42 -0
  260. package/src/time/time.schema.test.ts +237 -0
  261. package/src/time/timezone-independence.test.ts +188 -0
  262. package/src/time/timezone.types.test.ts +55 -0
  263. package/src/time/timezone.types.ts +22 -0
  264. package/tsconfig.json +26 -0
@@ -0,0 +1,122 @@
1
+ import dayjs from "dayjs";
2
+ import isoWeek from "dayjs/plugin/isoWeek.js";
3
+ import utc from "dayjs/plugin/utc.js";
4
+
5
+ import { ISO8601Schema } from "./iso8601.types";
6
+ import {
7
+ RelativeTimeSchema,
8
+ FlexibleTimeSchema,
9
+ FlexibleTime,
10
+ } from "./time-schemas";
11
+
12
+ dayjs.extend(isoWeek);
13
+ dayjs.extend(utc);
14
+
15
+ /**
16
+ * Parse a flexible time input (ISO 8601 or relative format) to ISO 8601 string
17
+ * Uses Zod schemas for validation instead of manual parsing
18
+ * All calculations are performed in UTC timezone
19
+ */
20
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Complex time parsing logic requires multiple conditional branches
21
+ export function parseFlexibleTime(
22
+ input: FlexibleTime,
23
+ baseTime: Date = new Date()
24
+ ): string {
25
+ // First, validate that input is a valid flexible time format
26
+ const flexibleResult = FlexibleTimeSchema.safeParse(input);
27
+ if (!flexibleResult.success) {
28
+ throw new Error(
29
+ `Invalid time format: ${input}. Expected ISO 8601 date, 'now', 'today', 'tomorrow', 'yesterday', relative format (1s, 2m, 3h, 4d, 5w, -1d, -2w), or week notation (week-start, week-end, week-start+1w, week-end-2w)`
30
+ );
31
+ }
32
+
33
+ // Try to parse as ISO 8601 first using Zod validation
34
+ const isoResult = ISO8601Schema.safeParse(input);
35
+ if (isoResult.success) {
36
+ return isoResult.data;
37
+ }
38
+
39
+ // Try to parse as relative time using Zod validation
40
+ const relativeResult = RelativeTimeSchema.safeParse(input);
41
+ if (relativeResult.success) {
42
+ // Check if it's week notation format
43
+ const weekMatch = input.match(/^week-(start|end)(([+-])\d+w)?$/);
44
+ if (weekMatch) {
45
+ const [, boundary, offsetPart] = weekMatch;
46
+ let weekOffset = 0;
47
+
48
+ // Parse offset if present (e.g., "+1w", "-2w")
49
+ if (offsetPart) {
50
+ const offsetMatch = offsetPart.match(/^([+-])(\d+)w$/);
51
+ if (offsetMatch) {
52
+ const [, sign, amount] = offsetMatch;
53
+ weekOffset = Number.parseInt(amount, 10);
54
+ if (sign === "-") {
55
+ weekOffset = -weekOffset;
56
+ }
57
+
58
+ // Zero offset is not allowed
59
+ if (weekOffset === 0) {
60
+ throw new Error(`Week offset cannot be zero: ${offsetPart}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ // Calculate week boundary based on type (in UTC)
66
+ return boundary === "start" ? dayjs.utc(baseTime).add(weekOffset, "week").startOf("isoWeek").toISOString() : dayjs.utc(baseTime).add(weekOffset, "week").endOf("isoWeek").endOf("day").toISOString();
67
+ }
68
+
69
+ // Handle literal values
70
+ if (input === "now") {
71
+ return baseTime.toISOString();
72
+ }
73
+
74
+ if (input === "today") {
75
+ return dayjs.utc(baseTime).startOf("day").toISOString();
76
+ }
77
+
78
+ // Map shortcuts to relative time format
79
+ let normalizedInput = input;
80
+ if (input === "tomorrow") {
81
+ normalizedInput = "1d";
82
+ } else if (input === "yesterday") {
83
+ normalizedInput = "-1d";
84
+ }
85
+
86
+ // Extract the validated parts using regex (since Zod already validated the format)
87
+ const match = normalizedInput.match(/^(-?\d+)([smhdw])$/);
88
+ if (!match) {
89
+ throw new Error(`Invalid time format: ${input}`);
90
+ }
91
+
92
+ const [, amountString, unit] = match;
93
+ const amount = Number.parseInt(amountString, 10);
94
+
95
+ if (amount === 0) {
96
+ throw new Error(`Time amount cannot be zero: ${amount}`);
97
+ }
98
+
99
+ // Map unit to Day.js unit names
100
+ const unitMap: Record<string, dayjs.ManipulateType> = {
101
+ s: "second",
102
+ m: "minute",
103
+ h: "hour",
104
+ d: "day",
105
+ w: "week",
106
+ };
107
+
108
+ const dayjsUnit = unitMap[unit];
109
+ return dayjs.utc(baseTime).add(amount, dayjsUnit).toISOString();
110
+ }
111
+
112
+ throw new Error(
113
+ `Invalid time format: ${input}. Expected ISO 8601 date, 'now', 'today', 'tomorrow', 'yesterday', relative format (1s, 2m, 3h, 4d, 5w, -1d, -2w), or week notation (week-start, week-end, week-start+1w, week-end-2w)`
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Create a flexible time parser with a specific base time (useful for testing)
119
+ */
120
+ export function createFlexibleTimeParser(baseTime: Date) {
121
+ return (input: FlexibleTime) => parseFlexibleTime(input, baseTime);
122
+ }
@@ -0,0 +1,243 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ FlexibleTimeFutureSchema,
5
+ FlexibleTimePastSchema,
6
+ FlexibleTimeSchema,
7
+ } from "./flexible-time.schema";
8
+
9
+ describe("FlexibleTimeSchema", () => {
10
+ describe("ISO8601 datetime validation", () => {
11
+ it("should accept valid ISO8601 datetime with Z", () => {
12
+ const result = FlexibleTimeSchema.safeParse("2024-01-15T12:00:00Z");
13
+ expect(result.success).toBe(true);
14
+ });
15
+
16
+ it("should accept valid ISO8601 datetime with timezone offset", () => {
17
+ const result = FlexibleTimeSchema.safeParse("2024-01-15T12:00:00+05:00");
18
+ expect(result.success).toBe(true);
19
+ });
20
+
21
+ it("should accept valid ISO8601 datetime with milliseconds", () => {
22
+ const result = FlexibleTimeSchema.safeParse("2024-01-15T12:00:00.123Z");
23
+ expect(result.success).toBe(true);
24
+ });
25
+
26
+ it("should reject date without time", () => {
27
+ const result = FlexibleTimeSchema.safeParse("2024-01-15");
28
+ expect(result.success).toBe(false);
29
+ });
30
+ });
31
+
32
+ describe("relative time validation", () => {
33
+ it("should accept minutes format", () => {
34
+ const result = FlexibleTimeSchema.safeParse("30m");
35
+ expect(result.success).toBe(true);
36
+ });
37
+
38
+ it("should accept hours format", () => {
39
+ const result = FlexibleTimeSchema.safeParse("3h");
40
+ expect(result.success).toBe(true);
41
+ });
42
+
43
+ it("should accept days format", () => {
44
+ const result = FlexibleTimeSchema.safeParse("7d");
45
+ expect(result.success).toBe(true);
46
+ });
47
+
48
+ it("should accept weeks format", () => {
49
+ const result = FlexibleTimeSchema.safeParse("2w");
50
+ expect(result.success).toBe(true);
51
+ });
52
+
53
+ it("should accept months format", () => {
54
+ const result = FlexibleTimeSchema.safeParse("1M");
55
+ expect(result.success).toBe(true);
56
+ });
57
+
58
+ it("should reject invalid relative time format", () => {
59
+ const result = FlexibleTimeSchema.safeParse("5x");
60
+ expect(result.success).toBe(false);
61
+ });
62
+
63
+ it("should reject relative time with spaces", () => {
64
+ const result = FlexibleTimeSchema.safeParse("5 d");
65
+ expect(result.success).toBe(false);
66
+ });
67
+
68
+ it("should reject relative time without amount", () => {
69
+ const result = FlexibleTimeSchema.safeParse("d");
70
+ expect(result.success).toBe(false);
71
+ });
72
+
73
+ it("should reject relative time without unit", () => {
74
+ const result = FlexibleTimeSchema.safeParse("5");
75
+ expect(result.success).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe("edge cases", () => {
80
+ it("should reject empty string", () => {
81
+ const result = FlexibleTimeSchema.safeParse("");
82
+ expect(result.success).toBe(false);
83
+ });
84
+
85
+ it("should reject invalid format", () => {
86
+ const result = FlexibleTimeSchema.safeParse("invalid");
87
+ expect(result.success).toBe(false);
88
+ });
89
+
90
+ it("should accept large relative time values", () => {
91
+ const result = FlexibleTimeSchema.safeParse("365d");
92
+ expect(result.success).toBe(true);
93
+ });
94
+ });
95
+ });
96
+
97
+ describe("FlexibleTimePastSchema", () => {
98
+ describe("transformation", () => {
99
+ it("should transform ISO8601 to normalized ISO8601", () => {
100
+ const result = FlexibleTimePastSchema.parse("2024-01-15T12:00:00Z");
101
+ expect(result).toBe("2024-01-15T12:00:00.000Z");
102
+ });
103
+
104
+ it("should transform relative time to ISO8601 (past direction)", () => {
105
+ const result = FlexibleTimePastSchema.parse("1d");
106
+ // Should be a valid ISO8601 string representing 1 day ago
107
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
108
+
109
+ // Verify it's in the past
110
+ const parsedDate = new Date(result);
111
+ const now = new Date();
112
+ expect(parsedDate.getTime()).toBeLessThan(now.getTime());
113
+ });
114
+
115
+ it("should transform minutes to ISO8601 (past)", () => {
116
+ const result = FlexibleTimePastSchema.parse("30m");
117
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
118
+ });
119
+
120
+ it("should transform hours to ISO8601 (past)", () => {
121
+ const result = FlexibleTimePastSchema.parse("3h");
122
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
123
+ });
124
+
125
+ it("should transform weeks to ISO8601 (past)", () => {
126
+ const result = FlexibleTimePastSchema.parse("2w");
127
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
128
+ });
129
+
130
+ it("should transform months to ISO8601 (past)", () => {
131
+ const result = FlexibleTimePastSchema.parse("1M");
132
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
133
+ });
134
+ });
135
+
136
+ describe("validation before transformation", () => {
137
+ it("should reject invalid formats", () => {
138
+ expect(() => FlexibleTimePastSchema.parse("invalid")).toThrow();
139
+ });
140
+
141
+ it("should reject malformed ISO8601", () => {
142
+ expect(() => FlexibleTimePastSchema.parse("2024-13-01")).toThrow();
143
+ });
144
+
145
+ it("should reject malformed relative time", () => {
146
+ expect(() => FlexibleTimePastSchema.parse("5x")).toThrow();
147
+ });
148
+ });
149
+ });
150
+
151
+ describe("FlexibleTimeFutureSchema", () => {
152
+ describe("transformation", () => {
153
+ it("should transform ISO8601 to normalized ISO8601", () => {
154
+ const result = FlexibleTimeFutureSchema.parse("2024-01-15T12:00:00Z");
155
+ expect(result).toBe("2024-01-15T12:00:00.000Z");
156
+ });
157
+
158
+ it("should transform relative time to ISO8601 (future direction)", () => {
159
+ const result = FlexibleTimeFutureSchema.parse("1d");
160
+ // Should be a valid ISO8601 string representing 1 day in future
161
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
162
+
163
+ // Verify it's in the future
164
+ const parsedDate = new Date(result);
165
+ const now = new Date();
166
+ expect(parsedDate.getTime()).toBeGreaterThan(now.getTime());
167
+ });
168
+
169
+ it("should transform minutes to ISO8601 (future)", () => {
170
+ const result = FlexibleTimeFutureSchema.parse("30m");
171
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
172
+
173
+ const parsedDate = new Date(result);
174
+ const now = new Date();
175
+ expect(parsedDate.getTime()).toBeGreaterThan(now.getTime());
176
+ });
177
+
178
+ it("should transform hours to ISO8601 (future)", () => {
179
+ const result = FlexibleTimeFutureSchema.parse("3h");
180
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
181
+
182
+ const parsedDate = new Date(result);
183
+ const now = new Date();
184
+ expect(parsedDate.getTime()).toBeGreaterThan(now.getTime());
185
+ });
186
+
187
+ it("should transform weeks to ISO8601 (future)", () => {
188
+ const result = FlexibleTimeFutureSchema.parse("2w");
189
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
190
+
191
+ const parsedDate = new Date(result);
192
+ const now = new Date();
193
+ expect(parsedDate.getTime()).toBeGreaterThan(now.getTime());
194
+ });
195
+
196
+ it("should transform months to ISO8601 (future)", () => {
197
+ const result = FlexibleTimeFutureSchema.parse("1M");
198
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
199
+
200
+ const parsedDate = new Date(result);
201
+ const now = new Date();
202
+ expect(parsedDate.getTime()).toBeGreaterThan(now.getTime());
203
+ });
204
+ });
205
+
206
+ describe("validation before transformation", () => {
207
+ it("should reject invalid formats", () => {
208
+ expect(() => FlexibleTimeFutureSchema.parse("invalid")).toThrow();
209
+ });
210
+
211
+ it("should reject malformed ISO8601", () => {
212
+ expect(() => FlexibleTimeFutureSchema.parse("2024-13-01")).toThrow();
213
+ });
214
+
215
+ it("should reject malformed relative time", () => {
216
+ expect(() => FlexibleTimeFutureSchema.parse("5x")).toThrow();
217
+ });
218
+ });
219
+ });
220
+
221
+ describe("direction comparison (Past vs Future)", () => {
222
+ it("should produce different results for same relative time input", () => {
223
+ const pastResult = FlexibleTimePastSchema.parse("1d");
224
+ const futureResult = FlexibleTimeFutureSchema.parse("1d");
225
+
226
+ expect(pastResult).not.toBe(futureResult);
227
+
228
+ const pastDate = new Date(pastResult);
229
+ const futureDate = new Date(futureResult);
230
+ const now = new Date();
231
+
232
+ expect(pastDate.getTime()).toBeLessThan(now.getTime());
233
+ expect(futureDate.getTime()).toBeGreaterThan(now.getTime());
234
+ });
235
+
236
+ it("should produce same result for ISO8601 input regardless of direction", () => {
237
+ const iso = "2024-01-15T12:00:00Z";
238
+ const pastResult = FlexibleTimePastSchema.parse(iso);
239
+ const futureResult = FlexibleTimeFutureSchema.parse(iso);
240
+
241
+ expect(pastResult).toBe(futureResult);
242
+ });
243
+ });
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+
3
+ import { ISO8601Schema } from "./iso8601.schema";
4
+ import { RelativeTimeSchema } from "./relative-time.schema";
5
+ import { parseFlexibleTime } from "./time-helpers";
6
+
7
+ /**
8
+ * Core validation schema for flexible time input
9
+ * Accepts either ISO8601 datetime or relative time expression
10
+ * Validates format but does not transform
11
+ */
12
+ export const FlexibleTimeSchema = z.union([ISO8601Schema, RelativeTimeSchema]);
13
+
14
+ export type FlexibleTime = z.infer<typeof FlexibleTimeSchema>;
15
+
16
+ /**
17
+ * Transformation schema that converts flexible time to ISO8601
18
+ * Direction: past (relative times subtract from current time)
19
+ * @example
20
+ * FlexibleTimePastSchema.parse("1d") // "2024-01-14T12:00:00.000Z"
21
+ * FlexibleTimePastSchema.parse("2024-01-15T12:00:00Z") // "2024-01-15T12:00:00.000Z"
22
+ */
23
+ export const FlexibleTimePastSchema = FlexibleTimeSchema.transform((value) =>
24
+ parseFlexibleTime(value, new Date(), "past")
25
+ );
26
+
27
+ /**
28
+ * Transformation schema that converts flexible time to ISO8601
29
+ * Direction: future (relative times add to current time)
30
+ * @example
31
+ * FlexibleTimeFutureSchema.parse("1d") // "2024-01-16T12:00:00.000Z"
32
+ * FlexibleTimeFutureSchema.parse("2024-01-15T12:00:00Z") // "2024-01-15T12:00:00.000Z"
33
+ */
34
+ export const FlexibleTimeFutureSchema = FlexibleTimeSchema.transform((value) =>
35
+ parseFlexibleTime(value, new Date(), "future")
36
+ );
37
+
38
+ /**
39
+ * Deprecated: Use FlexibleTimePastSchema instead
40
+ * @deprecated
41
+ */
42
+ export const SinceParameterSchema = FlexibleTimePastSchema;
43
+ export type SinceParameter = z.infer<typeof SinceParameterSchema>;
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { isRelativeTime } from "./is-relative-time";
4
+
5
+ describe("isRelativeTime", () => {
6
+ it("should identify valid relative time expressions", () => {
7
+ expect(isRelativeTime("1d")).toBe(true);
8
+ expect(isRelativeTime("30m")).toBe(true);
9
+ expect(isRelativeTime("2h")).toBe(true);
10
+ expect(isRelativeTime("1w")).toBe(true);
11
+ expect(isRelativeTime("3M")).toBe(true);
12
+ expect(isRelativeTime("999d")).toBe(true);
13
+ });
14
+
15
+ it("should reject invalid expressions", () => {
16
+ expect(isRelativeTime("2024-07-26T15:20:00Z")).toBe(false);
17
+ expect(isRelativeTime("invalid")).toBe(false);
18
+ expect(isRelativeTime("1x")).toBe(false);
19
+ expect(isRelativeTime("1day")).toBe(false);
20
+ expect(isRelativeTime("")).toBe(false);
21
+ expect(isRelativeTime("1.5d")).toBe(false);
22
+ });
23
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Check if a string is a relative time expression
3
+ *
4
+ * @param value - String to check
5
+ * @returns true if it's a relative time expression
6
+ */
7
+ export const isRelativeTime = (value: string): boolean => {
8
+ return /^(\d+)[mhdwM]$/.test(value);
9
+ };
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Core validation schema for ISO8601 datetime strings
5
+ * Validates format but does not transform
6
+ */
7
+ export const ISO8601Schema = z
8
+ .string()
9
+ .describe("Datetime in ISO8601 format")
10
+ .regex(
11
+ /^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$/,
12
+ "Must be a valid ISO8601 datetime (e.g., '2024-01-01T12:00:00Z')"
13
+ );
14
+
15
+ export type ISO8601 = z.infer<typeof ISO8601Schema>;
16
+
17
+ /**
18
+ * Core validation schema for ISO8601 date strings (YYYY-MM-DD)
19
+ * Validates format but does not transform
20
+ */
21
+ export const ISO8601DateSchema = z
22
+ .string()
23
+ .describe("Date in ISO8601 format")
24
+ .regex(
25
+ /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,
26
+ "Date must be in YYYY-MM-DD format with valid month (01-12) and day (01-31)"
27
+ );
28
+
29
+ export type ISO8601Date = z.infer<typeof ISO8601DateSchema>;
@@ -0,0 +1,112 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { ZodError } from "zod";
3
+
4
+ import { ISO8601Schema, ISO8601DateSchema } from "./iso8601.types";
5
+
6
+ describe("ISO8601Schema", () => {
7
+ describe("valid formats", () => {
8
+ test("accepts full ISO8601 format with Z timezone", () => {
9
+ const dateString = "2023-04-12T14:30:45Z";
10
+ const result = ISO8601Schema.parse(dateString);
11
+ expect(result).toBe(new Date(dateString).toISOString());
12
+ });
13
+
14
+ test("accepts ISO8601 with milliseconds", () => {
15
+ const dateString = "2023-04-12T14:30:45.123Z";
16
+ const result = ISO8601Schema.parse(dateString);
17
+ expect(result).toBe(new Date(dateString).toISOString());
18
+ });
19
+
20
+ test("accepts ISO8601 with positive timezone offset", () => {
21
+ const dateString = "2023-04-12T14:30:45+02:00";
22
+ const result = ISO8601Schema.parse(dateString);
23
+ expect(result).toBe(new Date(dateString).toISOString());
24
+ });
25
+
26
+ test("accepts ISO8601 with negative timezone offset", () => {
27
+ const dateString = "2023-04-12T14:30:45-05:00";
28
+ const result = ISO8601Schema.parse(dateString);
29
+ expect(result).toBe(new Date(dateString).toISOString());
30
+ });
31
+ });
32
+
33
+ describe("invalid formats", () => {
34
+ test("rejects non-ISO8601 string", () => {
35
+ const dateString = "April 12, 2023";
36
+ expect(() => ISO8601Schema.parse(dateString)).toThrow(ZodError);
37
+ });
38
+
39
+ test("rejects date without time", () => {
40
+ const dateString = "2023-04-12";
41
+ expect(() => ISO8601Schema.parse(dateString)).toThrow(ZodError);
42
+ });
43
+
44
+ test("rejects time without date", () => {
45
+ const dateString = "14:30:45Z";
46
+ expect(() => ISO8601Schema.parse(dateString)).toThrow(ZodError);
47
+ });
48
+
49
+ test("rejects malformed ISO8601 (incorrect separators)", () => {
50
+ const dateString = "2023/04/12T14:30:45Z";
51
+ expect(() => ISO8601Schema.parse(dateString)).toThrow(ZodError);
52
+ });
53
+
54
+ test("rejects malformed ISO8601 (incorrect time format)", () => {
55
+ const dateString = "2023-04-12T14-30-45Z";
56
+ expect(() => ISO8601Schema.parse(dateString)).toThrow(ZodError);
57
+ });
58
+ });
59
+
60
+ describe("transformation", () => {
61
+ test("transforms valid ISO8601 to date ISO string", () => {
62
+ const dateString = "2023-04-12T14:30:45Z";
63
+ const result = ISO8601Schema.parse(dateString);
64
+ expect(typeof result).toBe("string");
65
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
66
+ expect(result).toBe(new Date(dateString).toISOString());
67
+ });
68
+ });
69
+ });
70
+
71
+ describe("ISO8601DateSchema", () => {
72
+ describe("valid formats", () => {
73
+ test("accepts valid ISO8601 date format", () => {
74
+ const dateString = "2023-04-12";
75
+ const result = ISO8601DateSchema.parse(dateString);
76
+ expect(result).toBe(dateString);
77
+ });
78
+
79
+ test("accepts date with leading zeros", () => {
80
+ const dateString = "2023-01-05";
81
+ const result = ISO8601DateSchema.parse(dateString);
82
+ expect(result).toBe(dateString);
83
+ });
84
+ });
85
+
86
+ describe("invalid formats", () => {
87
+ test("rejects non-ISO8601 date string", () => {
88
+ const dateString = "04/12/2023";
89
+ expect(() => ISO8601DateSchema.parse(dateString)).toThrow(ZodError);
90
+ });
91
+
92
+ test("rejects date with time", () => {
93
+ const dateString = "2023-04-12T14:30:45Z";
94
+ expect(() => ISO8601DateSchema.parse(dateString)).toThrow(ZodError);
95
+ });
96
+
97
+ test("rejects date with incorrect separator", () => {
98
+ const dateString = "2023/04/12";
99
+ expect(() => ISO8601DateSchema.parse(dateString)).toThrow(ZodError);
100
+ });
101
+
102
+ test("rejects malformed date format", () => {
103
+ const dateString = "20230412";
104
+ expect(() => ISO8601DateSchema.parse(dateString)).toThrow(ZodError);
105
+ });
106
+
107
+ test("rejects invalid date values", () => {
108
+ const dateString = "2023-13-45"; // Invalid month and day
109
+ expect(() => ISO8601DateSchema.parse(dateString)).toThrow(ZodError);
110
+ });
111
+ });
112
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+
3
+ export const ISO8601Schema = z
4
+ .string()
5
+ .describe("Datetime in ISO8601 format")
6
+ .regex(
7
+ /^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$/
8
+ )
9
+ .transform((v) => new Date(v).toISOString());
10
+
11
+ export type ISO8601 = z.infer<typeof ISO8601Schema>;
12
+
13
+ // ensure valid month and valid day
14
+ export const ISO8601DateSchema = z
15
+ .string()
16
+ .describe("Date in ISO8601 format")
17
+ .regex(
18
+ /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,
19
+ "Date must be in YYYY-MM-DD format with valid month (01-12) and day (01-31)"
20
+ );
21
+ export type ISO8601Date = z.infer<typeof ISO8601DateSchema>;
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+
3
+ import { parseRelativeTime } from "./parse-relative-time";
4
+
5
+ describe("parseRelativeTime", () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ vi.setSystemTime(new Date("2024-07-26T15:20:00Z"));
9
+ });
10
+
11
+ afterEach(() => {
12
+ vi.useRealTimers();
13
+ });
14
+
15
+ it("should parse minutes correctly", () => {
16
+ expect(parseRelativeTime("30m")).toBe("2024-07-26T14:50:00.000Z");
17
+ expect(parseRelativeTime("90m")).toBe("2024-07-26T13:50:00.000Z");
18
+ });
19
+
20
+ it("should parse hours correctly", () => {
21
+ expect(parseRelativeTime("1h")).toBe("2024-07-26T14:20:00.000Z");
22
+ expect(parseRelativeTime("24h")).toBe("2024-07-25T15:20:00.000Z");
23
+ });
24
+
25
+ it("should parse days correctly", () => {
26
+ expect(parseRelativeTime("1d")).toBe("2024-07-25T15:20:00.000Z");
27
+ expect(parseRelativeTime("7d")).toBe("2024-07-19T15:20:00.000Z");
28
+ });
29
+
30
+ it("should parse weeks correctly", () => {
31
+ expect(parseRelativeTime("1w")).toBe("2024-07-19T15:20:00.000Z");
32
+ expect(parseRelativeTime("2w")).toBe("2024-07-12T15:20:00.000Z");
33
+ });
34
+
35
+ it("should parse months correctly", () => {
36
+ expect(parseRelativeTime("1M")).toBe("2024-06-26T15:20:00.000Z");
37
+ expect(parseRelativeTime("2M")).toBe("2024-05-26T15:20:00.000Z");
38
+ });
39
+
40
+ it("should throw error for invalid formats", () => {
41
+ expect(() => parseRelativeTime("invalid")).toThrow(
42
+ "Invalid relative time format"
43
+ );
44
+ expect(() => parseRelativeTime("1x")).toThrow(
45
+ "Invalid relative time format"
46
+ );
47
+ expect(() => parseRelativeTime("")).toThrow("Invalid relative time format");
48
+ });
49
+ });