@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,181 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { RelativeTimeSchema, FlexibleTimeSchema } from "./time-schemas";
4
+
5
+ describe("time-schemas", () => {
6
+ describe("RelativeTimeSchema", () => {
7
+ describe("valid formats", () => {
8
+ it("should validate correct relative time formats", () => {
9
+ expect(() => RelativeTimeSchema.parse("1s")).not.toThrow();
10
+ expect(() => RelativeTimeSchema.parse("30m")).not.toThrow();
11
+ expect(() => RelativeTimeSchema.parse("24h")).not.toThrow();
12
+ expect(() => RelativeTimeSchema.parse("7d")).not.toThrow();
13
+ expect(() => RelativeTimeSchema.parse("2w")).not.toThrow();
14
+ });
15
+
16
+ it("should accept 'now' literal", () => {
17
+ expect(() => RelativeTimeSchema.parse("now")).not.toThrow();
18
+ });
19
+
20
+ it("should accept 'today' literal", () => {
21
+ expect(() => RelativeTimeSchema.parse("today")).not.toThrow();
22
+ });
23
+
24
+ it("should accept various numeric amounts", () => {
25
+ expect(() => RelativeTimeSchema.parse("1s")).not.toThrow();
26
+ expect(() => RelativeTimeSchema.parse("999s")).not.toThrow();
27
+ expect(() => RelativeTimeSchema.parse("100d")).not.toThrow();
28
+ });
29
+
30
+ it("should accept negative relative times", () => {
31
+ expect(() => RelativeTimeSchema.parse("-1s")).not.toThrow();
32
+ expect(() => RelativeTimeSchema.parse("-30m")).not.toThrow();
33
+ expect(() => RelativeTimeSchema.parse("-24h")).not.toThrow();
34
+ expect(() => RelativeTimeSchema.parse("-7d")).not.toThrow();
35
+ expect(() => RelativeTimeSchema.parse("-2w")).not.toThrow();
36
+ });
37
+
38
+ it("should accept week notation without offset", () => {
39
+ expect(() => RelativeTimeSchema.parse("week-start")).not.toThrow();
40
+ expect(() => RelativeTimeSchema.parse("week-end")).not.toThrow();
41
+ });
42
+
43
+ it("should accept week notation with positive offset", () => {
44
+ expect(() => RelativeTimeSchema.parse("week-start+1w")).not.toThrow();
45
+ expect(() => RelativeTimeSchema.parse("week-start+2w")).not.toThrow();
46
+ expect(() => RelativeTimeSchema.parse("week-end+1w")).not.toThrow();
47
+ expect(() => RelativeTimeSchema.parse("week-end+10w")).not.toThrow();
48
+ });
49
+
50
+ it("should accept week notation with negative offset", () => {
51
+ expect(() => RelativeTimeSchema.parse("week-start-1w")).not.toThrow();
52
+ expect(() => RelativeTimeSchema.parse("week-start-2w")).not.toThrow();
53
+ expect(() => RelativeTimeSchema.parse("week-end-1w")).not.toThrow();
54
+ expect(() => RelativeTimeSchema.parse("week-end-10w")).not.toThrow();
55
+ });
56
+ });
57
+
58
+ describe("invalid formats", () => {
59
+ it("should reject formats without numbers", () => {
60
+ expect(() => RelativeTimeSchema.parse("m")).toThrow();
61
+ expect(() => RelativeTimeSchema.parse("h")).toThrow();
62
+ expect(() => RelativeTimeSchema.parse("d")).toThrow();
63
+ });
64
+
65
+ it("should reject formats without units", () => {
66
+ expect(() => RelativeTimeSchema.parse("1")).toThrow();
67
+ expect(() => RelativeTimeSchema.parse("123")).toThrow();
68
+ });
69
+
70
+ it("should reject invalid units", () => {
71
+ expect(() => RelativeTimeSchema.parse("1x")).toThrow();
72
+ expect(() => RelativeTimeSchema.parse("1y")).toThrow();
73
+ expect(() => RelativeTimeSchema.parse("1month")).toThrow();
74
+ });
75
+
76
+ it("should reject decimal amounts", () => {
77
+ expect(() => RelativeTimeSchema.parse("1.5h")).toThrow();
78
+ expect(() => RelativeTimeSchema.parse("0.5d")).toThrow();
79
+ expect(() => RelativeTimeSchema.parse("-1.5h")).toThrow();
80
+ expect(() => RelativeTimeSchema.parse("-0.5d")).toThrow();
81
+ });
82
+
83
+ it("should be case-sensitive for literals", () => {
84
+ expect(() => RelativeTimeSchema.parse("Now")).toThrow();
85
+ expect(() => RelativeTimeSchema.parse("NOW")).toThrow();
86
+ expect(() => RelativeTimeSchema.parse("Today")).toThrow();
87
+ expect(() => RelativeTimeSchema.parse("TODAY")).toThrow();
88
+ });
89
+
90
+ it("should reject empty strings", () => {
91
+ expect(() => RelativeTimeSchema.parse("")).toThrow();
92
+ });
93
+
94
+ it("should reject whitespace", () => {
95
+ expect(() => RelativeTimeSchema.parse(" ")).toThrow();
96
+ expect(() => RelativeTimeSchema.parse("1 h")).toThrow();
97
+ expect(() => RelativeTimeSchema.parse(" 1h")).toThrow();
98
+ expect(() => RelativeTimeSchema.parse("1h ")).toThrow();
99
+ });
100
+
101
+ it("should reject invalid week notation formats", () => {
102
+ expect(() => RelativeTimeSchema.parse("week-middle")).toThrow();
103
+ expect(() => RelativeTimeSchema.parse("week-begin")).toThrow();
104
+ expect(() => RelativeTimeSchema.parse("weekstart")).toThrow();
105
+ expect(() => RelativeTimeSchema.parse("week-start+1d")).toThrow();
106
+ expect(() => RelativeTimeSchema.parse("week-end+2h")).toThrow();
107
+ expect(() => RelativeTimeSchema.parse("week-start1w")).toThrow();
108
+ expect(() => RelativeTimeSchema.parse("week-start+")).toThrow();
109
+ expect(() => RelativeTimeSchema.parse("week-end-")).toThrow();
110
+ });
111
+ });
112
+ });
113
+
114
+ describe("FlexibleTimeSchema", () => {
115
+ describe("ISO 8601 dates", () => {
116
+ it("should accept valid ISO 8601 dates", () => {
117
+ expect(() =>
118
+ FlexibleTimeSchema.parse("2024-01-01T00:00:00.000Z")
119
+ ).not.toThrow();
120
+ expect(() =>
121
+ FlexibleTimeSchema.parse("2024-01-01T12:30:45Z")
122
+ ).not.toThrow();
123
+ expect(() =>
124
+ FlexibleTimeSchema.parse("2024-12-31T23:59:59.999Z")
125
+ ).not.toThrow();
126
+ });
127
+
128
+ it("should accept ISO 8601 with timezone offsets", () => {
129
+ expect(() =>
130
+ FlexibleTimeSchema.parse("2024-01-01T12:00:00+01:00")
131
+ ).not.toThrow();
132
+ expect(() =>
133
+ FlexibleTimeSchema.parse("2024-01-01T12:00:00-05:00")
134
+ ).not.toThrow();
135
+ });
136
+ });
137
+
138
+ describe("relative time formats", () => {
139
+ it("should accept relative time formats", () => {
140
+ expect(() => FlexibleTimeSchema.parse("1h")).not.toThrow();
141
+ expect(() => FlexibleTimeSchema.parse("30m")).not.toThrow();
142
+ expect(() => FlexibleTimeSchema.parse("7d")).not.toThrow();
143
+ expect(() => FlexibleTimeSchema.parse("2w")).not.toThrow();
144
+ });
145
+
146
+ it("should accept negative relative time formats", () => {
147
+ expect(() => FlexibleTimeSchema.parse("-1h")).not.toThrow();
148
+ expect(() => FlexibleTimeSchema.parse("-30m")).not.toThrow();
149
+ expect(() => FlexibleTimeSchema.parse("-7d")).not.toThrow();
150
+ expect(() => FlexibleTimeSchema.parse("-2w")).not.toThrow();
151
+ });
152
+ });
153
+
154
+ describe("literals", () => {
155
+ it("should accept 'now' and 'today' literals", () => {
156
+ expect(() => FlexibleTimeSchema.parse("now")).not.toThrow();
157
+ expect(() => FlexibleTimeSchema.parse("today")).not.toThrow();
158
+ });
159
+
160
+ it("should accept week notation", () => {
161
+ expect(() => FlexibleTimeSchema.parse("week-start")).not.toThrow();
162
+ expect(() => FlexibleTimeSchema.parse("week-end")).not.toThrow();
163
+ expect(() => FlexibleTimeSchema.parse("week-start+1w")).not.toThrow();
164
+ expect(() => FlexibleTimeSchema.parse("week-end-2w")).not.toThrow();
165
+ });
166
+ });
167
+
168
+ describe("invalid formats", () => {
169
+ it("should reject invalid formats", () => {
170
+ expect(() => FlexibleTimeSchema.parse("invalid")).toThrow();
171
+ expect(() => FlexibleTimeSchema.parse("")).toThrow();
172
+ expect(() => FlexibleTimeSchema.parse("not a date")).toThrow();
173
+ });
174
+
175
+ it("should reject partial ISO 8601 dates", () => {
176
+ expect(() => FlexibleTimeSchema.parse("2024-01-01")).toThrow();
177
+ expect(() => FlexibleTimeSchema.parse("2024-01")).toThrow();
178
+ });
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+
3
+ import { ISO8601Schema } from "./iso8601.types";
4
+
5
+ export type TimeUnit = "s" | "m" | "h" | "d" | "w";
6
+
7
+ export interface RelativeTimeConfig {
8
+ amount: number;
9
+ unit: TimeUnit;
10
+ }
11
+
12
+ export const RelativeTimeSchema = z.union([
13
+ z.literal("now"),
14
+ z.literal("today"),
15
+ z.literal("tomorrow"),
16
+ z.literal("yesterday"),
17
+ z
18
+ .string()
19
+ .regex(
20
+ /^(-?\d+)([smhdw])$/,
21
+ "Invalid relative time format. Use format like '1m' (1 min future), '2h' (2 hours future), '3d' (3 days future), '-1d' (1 day past), '-2w' (2 weeks past)"
22
+ ),
23
+ z
24
+ .string()
25
+ .regex(
26
+ /^week-(start|end)(([+-])\d+w)?$/,
27
+ "Invalid week notation format. Use format like 'week-start', 'week-end', 'week-start+1w', 'week-end-2w'"
28
+ ),
29
+ ]);
30
+
31
+ /**
32
+ * Flexible time schema that accepts either ISO 8601 dates or relative time formats
33
+ *
34
+ * Supports:
35
+ * - ISO 8601 dates: "2024-01-01T12:00:00Z"
36
+ * - Literals: "now" (current datetime), "today" (current day at 00:00:00), "tomorrow" (next day), "yesterday" (previous day)
37
+ * - Relative future times: "30s", "5m", "2h", "3d", "1w" (positive = future)
38
+ * - Relative past times: "-1d" (yesterday), "-2w" (2 weeks ago) (negative = past)
39
+ * - Week notation: "week-start" (Monday 00:00:00), "week-end" (Sunday 23:59:59), "week-start+1w" (next week start), "week-end-2w" (end of 2 weeks ago)
40
+ */
41
+ export const FlexibleTimeSchema = z.union([ISO8601Schema, RelativeTimeSchema]);
42
+ export type FlexibleTime = z.infer<typeof FlexibleTimeSchema>;
@@ -0,0 +1,237 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+
3
+ import { createBoundedFlexibleTimeSchema, createBoundedRelativeTimeSchema } from "./bounded-time.schema";
4
+ import {
5
+ FlexibleTimeSchema,
6
+ FlexibleTimePastSchema,
7
+ FlexibleTimeFutureSchema,
8
+ } from "./flexible-time.schema";
9
+ import { ISO8601Schema, ISO8601DateSchema } from "./iso8601.schema";
10
+ import {
11
+ RelativeTimeSchema,
12
+ RelativeTimePastSchema,
13
+ RelativeTimeFutureSchema,
14
+ } from "./relative-time.schema";
15
+ import { parseRelativeTime, parseFlexibleTime } from "./time-helpers";
16
+
17
+ describe("time.schema", () => {
18
+ beforeEach(() => {
19
+ vi.useFakeTimers();
20
+ vi.setSystemTime(new Date("2024-07-26T15:20:00Z"));
21
+ });
22
+
23
+ afterEach(() => {
24
+ vi.useRealTimers();
25
+ });
26
+
27
+ describe("ISO8601Schema", () => {
28
+ it("should accept valid ISO8601 timestamps", () => {
29
+ expect(ISO8601Schema.parse("2024-07-26T15:20:00Z")).toBe(
30
+ "2024-07-26T15:20:00Z"
31
+ );
32
+ expect(ISO8601Schema.parse("2024-07-26T15:20:00.000Z")).toBe(
33
+ "2024-07-26T15:20:00.000Z"
34
+ );
35
+ expect(ISO8601Schema.parse("2024-07-26T17:20:00+02:00")).toBe(
36
+ "2024-07-26T17:20:00+02:00"
37
+ );
38
+ });
39
+
40
+ it("should reject invalid ISO8601 formats", () => {
41
+ expect(() => ISO8601Schema.parse("2024-07-26")).toThrow();
42
+ expect(() => ISO8601Schema.parse("invalid")).toThrow();
43
+ });
44
+ });
45
+
46
+ describe("ISO8601DateSchema", () => {
47
+ it("should accept valid date formats", () => {
48
+ expect(ISO8601DateSchema.parse("2024-07-26")).toBe("2024-07-26");
49
+ expect(ISO8601DateSchema.parse("2024-01-01")).toBe("2024-01-01");
50
+ });
51
+
52
+ it("should reject invalid dates", () => {
53
+ expect(() => ISO8601DateSchema.parse("2024-13-01")).toThrow();
54
+ expect(() => ISO8601DateSchema.parse("2024-01-32")).toThrow();
55
+ expect(() => ISO8601DateSchema.parse("2024-07-26T15:20:00Z")).toThrow();
56
+ });
57
+ });
58
+
59
+ describe("RelativeTimeSchema", () => {
60
+ it("should accept valid relative time expressions", () => {
61
+ expect(RelativeTimeSchema.parse("1d")).toBe("1d");
62
+ expect(RelativeTimeSchema.parse("3h")).toBe("3h");
63
+ expect(RelativeTimeSchema.parse("30m")).toBe("30m");
64
+ expect(RelativeTimeSchema.parse("1w")).toBe("1w");
65
+ expect(RelativeTimeSchema.parse("2M")).toBe("2M");
66
+ });
67
+
68
+ it("should reject invalid formats", () => {
69
+ expect(() => RelativeTimeSchema.parse("1")).toThrow();
70
+ expect(() => RelativeTimeSchema.parse("d")).toThrow();
71
+ expect(() => RelativeTimeSchema.parse("1x")).toThrow();
72
+ });
73
+ });
74
+
75
+ describe("FlexibleTimeSchema", () => {
76
+ it("should accept ISO8601 timestamps", () => {
77
+ expect(FlexibleTimeSchema.parse("2024-07-26T15:20:00Z")).toBe(
78
+ "2024-07-26T15:20:00Z"
79
+ );
80
+ });
81
+
82
+ it("should accept relative time expressions", () => {
83
+ expect(FlexibleTimeSchema.parse("1d")).toBe("1d");
84
+ expect(FlexibleTimeSchema.parse("3h")).toBe("3h");
85
+ });
86
+
87
+ it("should reject invalid formats", () => {
88
+ expect(() => FlexibleTimeSchema.parse("invalid")).toThrow();
89
+ });
90
+ });
91
+
92
+ describe("parseRelativeTime", () => {
93
+ it("should parse relative time in past direction", () => {
94
+ expect(parseRelativeTime("1d", new Date("2024-07-26T15:20:00Z"), "past")).toBe(
95
+ "2024-07-25T15:20:00.000Z"
96
+ );
97
+ expect(parseRelativeTime("30m", new Date("2024-07-26T15:20:00Z"), "past")).toBe(
98
+ "2024-07-26T14:50:00.000Z"
99
+ );
100
+ });
101
+
102
+ it("should parse relative time in future direction", () => {
103
+ expect(parseRelativeTime("1d", new Date("2024-07-26T15:20:00Z"), "future")).toBe(
104
+ "2024-07-27T15:20:00.000Z"
105
+ );
106
+ expect(parseRelativeTime("2h", new Date("2024-07-26T15:20:00Z"), "future")).toBe(
107
+ "2024-07-26T17:20:00.000Z"
108
+ );
109
+ });
110
+
111
+ it("should handle all time units", () => {
112
+ const base = new Date("2024-07-26T15:20:00Z");
113
+ expect(parseRelativeTime("30m", base, "past")).toBe("2024-07-26T14:50:00.000Z");
114
+ expect(parseRelativeTime("2h", base, "past")).toBe("2024-07-26T13:20:00.000Z");
115
+ expect(parseRelativeTime("3d", base, "past")).toBe("2024-07-23T15:20:00.000Z");
116
+ expect(parseRelativeTime("1w", base, "past")).toBe("2024-07-19T15:20:00.000Z");
117
+ expect(parseRelativeTime("1M", base, "past")).toBe("2024-06-26T15:20:00.000Z");
118
+ });
119
+ });
120
+
121
+ describe("parseFlexibleTime", () => {
122
+ it("should parse ISO8601 timestamps", () => {
123
+ expect(parseFlexibleTime("2024-07-26T15:20:00Z")).toBe(
124
+ "2024-07-26T15:20:00.000Z"
125
+ );
126
+ });
127
+
128
+ it("should parse relative times", () => {
129
+ expect(parseFlexibleTime("1d", new Date("2024-07-26T15:20:00Z"), "past")).toBe(
130
+ "2024-07-25T15:20:00.000Z"
131
+ );
132
+ expect(parseFlexibleTime("2h", new Date("2024-07-26T15:20:00Z"), "future")).toBe(
133
+ "2024-07-26T17:20:00.000Z"
134
+ );
135
+ });
136
+ });
137
+
138
+ describe("RelativeTimePastSchema", () => {
139
+ it("should transform relative time to ISO8601 (past direction)", () => {
140
+ expect(RelativeTimePastSchema.parse("1d")).toBe("2024-07-25T15:20:00.000Z");
141
+ expect(RelativeTimePastSchema.parse("30m")).toBe("2024-07-26T14:50:00.000Z");
142
+ expect(RelativeTimePastSchema.parse("1w")).toBe("2024-07-19T15:20:00.000Z");
143
+ });
144
+ });
145
+
146
+ describe("RelativeTimeFutureSchema", () => {
147
+ it("should transform relative time to ISO8601 (future direction)", () => {
148
+ expect(RelativeTimeFutureSchema.parse("1d")).toBe("2024-07-27T15:20:00.000Z");
149
+ expect(RelativeTimeFutureSchema.parse("2h")).toBe("2024-07-26T17:20:00.000Z");
150
+ });
151
+ });
152
+
153
+ describe("FlexibleTimePastSchema", () => {
154
+ it("should transform ISO8601 timestamps", () => {
155
+ expect(FlexibleTimePastSchema.parse("2024-07-26T15:20:00Z")).toBe(
156
+ "2024-07-26T15:20:00.000Z"
157
+ );
158
+ });
159
+
160
+ it("should transform relative times (past direction)", () => {
161
+ expect(FlexibleTimePastSchema.parse("1d")).toBe("2024-07-25T15:20:00.000Z");
162
+ expect(FlexibleTimePastSchema.parse("3h")).toBe("2024-07-26T12:20:00.000Z");
163
+ });
164
+ });
165
+
166
+ describe("FlexibleTimeFutureSchema", () => {
167
+ it("should transform ISO8601 timestamps", () => {
168
+ expect(FlexibleTimeFutureSchema.parse("2024-07-26T15:20:00Z")).toBe(
169
+ "2024-07-26T15:20:00.000Z"
170
+ );
171
+ });
172
+
173
+ it("should transform relative times (future direction)", () => {
174
+ expect(FlexibleTimeFutureSchema.parse("1d")).toBe("2024-07-27T15:20:00.000Z");
175
+ expect(FlexibleTimeFutureSchema.parse("2h")).toBe("2024-07-26T17:20:00.000Z");
176
+ });
177
+ });
178
+
179
+ describe("createBoundedFlexibleTimeSchema", () => {
180
+ it("should enforce minimum boundary (oldest allowed)", () => {
181
+ const schema = createBoundedFlexibleTimeSchema({ min: "1w" });
182
+
183
+ // Should accept times more recent than 1w ago
184
+ expect(() => schema.parse("3d")).not.toThrow();
185
+
186
+ // Should reject times older than 1w ago
187
+ expect(() => schema.parse("2w")).toThrow(/Time must be more recent than/);
188
+ });
189
+
190
+ it("should enforce maximum boundary (newest allowed)", () => {
191
+ const schema = createBoundedFlexibleTimeSchema({ max: "1d" });
192
+
193
+ // Should accept times before max (older than 1d in future)
194
+ expect(() => schema.parse("1w")).not.toThrow();
195
+
196
+ // Max in future means we accept past times, but we're using FlexibleTimePastSchema
197
+ // so it processes relative times as "going back"
198
+ expect(() => schema.parse("2024-10-01T00:00:00Z")).toThrow(/Time must be before/);
199
+ });
200
+
201
+ it("should enforce both min and max boundaries", () => {
202
+ const schema = createBoundedFlexibleTimeSchema({ min: "2w", max: "1d" });
203
+
204
+ // Should accept times within range
205
+ expect(() => schema.parse("3d")).not.toThrow();
206
+ expect(() => schema.parse("1w")).not.toThrow();
207
+
208
+ // Should reject times outside range
209
+ expect(() => schema.parse("3w")).toThrow();
210
+ });
211
+
212
+ it("should work with ISO8601 inputs", () => {
213
+ const schema = createBoundedFlexibleTimeSchema({ min: "1w", max: "1w" });
214
+
215
+ expect(() => schema.parse("2024-07-23T15:20:00Z")).not.toThrow();
216
+ });
217
+ });
218
+
219
+ describe("createBoundedRelativeTimeSchema", () => {
220
+ it("should enforce minimum boundary", () => {
221
+ const schema = createBoundedRelativeTimeSchema({ min: "1h", max: "30d" });
222
+
223
+ // Should accept times within range
224
+ expect(() => schema.parse("1d")).not.toThrow();
225
+
226
+ // Should reject times outside range
227
+ expect(() => schema.parse("60d")).toThrow();
228
+ });
229
+
230
+ it("should work with all time units", () => {
231
+ const schema = createBoundedRelativeTimeSchema({ min: "1h", max: "1M" });
232
+
233
+ expect(() => schema.parse("1d")).not.toThrow();
234
+ expect(() => schema.parse("30m")).toThrow(); // Less than min
235
+ });
236
+ });
237
+ });
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ import { parseFlexibleTime } from "./flexible-time-parser";
4
+ import { parseRelativeTime } from "./parse-relative-time";
5
+
6
+ /**
7
+ * Timezone independence tests
8
+ * These tests verify that all time calculations produce identical results
9
+ * regardless of the system timezone setting.
10
+ */
11
+ describe("timezone independence", () => {
12
+ beforeEach(() => {
13
+ vi.useFakeTimers();
14
+ // Set a fixed time for consistent testing
15
+ vi.setSystemTime(new Date("2024-01-15T12:00:00.000Z"));
16
+ });
17
+
18
+ afterEach(() => {
19
+ vi.useRealTimers();
20
+ });
21
+
22
+ const baseTime = new Date("2024-01-15T12:00:00.000Z");
23
+
24
+ describe("flexible time parser", () => {
25
+ it("should return UTC midnight for 'today' regardless of local timezone", () => {
26
+ // This test verifies the fix for the "today" literal bug
27
+ // Before fix: would use local Date object causing timezone-dependent results
28
+ // After fix: uses dayjs.utc().startOf('day') for consistent UTC results
29
+ const result = parseFlexibleTime("today", baseTime);
30
+ expect(result).toBe("2024-01-15T00:00:00.000Z");
31
+
32
+ // Verify the exact time components in UTC
33
+ const date = new Date(result);
34
+ expect(date.getUTCHours()).toBe(0);
35
+ expect(date.getUTCMinutes()).toBe(0);
36
+ expect(date.getUTCSeconds()).toBe(0);
37
+ expect(date.getUTCMilliseconds()).toBe(0);
38
+ });
39
+
40
+ it("should calculate week boundaries in UTC", () => {
41
+ // Monday 00:00:00 UTC (start of ISO week)
42
+ const weekStart = parseFlexibleTime("week-start", baseTime);
43
+ expect(weekStart).toBe("2024-01-15T00:00:00.000Z");
44
+
45
+ const startDate = new Date(weekStart);
46
+ expect(startDate.getUTCDay()).toBe(1); // Monday
47
+ expect(startDate.getUTCHours()).toBe(0);
48
+ expect(startDate.getUTCMinutes()).toBe(0);
49
+
50
+ // Sunday 23:59:59.999 UTC (end of ISO week)
51
+ const weekEnd = parseFlexibleTime("week-end", baseTime);
52
+ expect(weekEnd).toBe("2024-01-21T23:59:59.999Z");
53
+
54
+ const endDate = new Date(weekEnd);
55
+ expect(endDate.getUTCDay()).toBe(0); // Sunday
56
+ expect(endDate.getUTCHours()).toBe(23);
57
+ expect(endDate.getUTCMinutes()).toBe(59);
58
+ expect(endDate.getUTCSeconds()).toBe(59);
59
+ expect(endDate.getUTCMilliseconds()).toBe(999);
60
+ });
61
+
62
+ it("should add relative time in UTC without DST interference", () => {
63
+ // These operations should add exact milliseconds in UTC
64
+ // No DST adjustments should occur
65
+ expect(parseFlexibleTime("1d", baseTime)).toBe("2024-01-16T12:00:00.000Z");
66
+ expect(parseFlexibleTime("1w", baseTime)).toBe("2024-01-22T12:00:00.000Z");
67
+ expect(parseFlexibleTime("-1d", baseTime)).toBe("2024-01-14T12:00:00.000Z");
68
+ expect(parseFlexibleTime("-1w", baseTime)).toBe("2024-01-08T12:00:00.000Z");
69
+ });
70
+
71
+ it("should handle DST boundary dates correctly", () => {
72
+ // US DST transition: March 10, 2024 at 2:00 AM (spring forward)
73
+ const beforeDST = new Date("2024-03-09T12:00:00.000Z");
74
+ const afterDST = new Date("2024-03-11T12:00:00.000Z");
75
+
76
+ // Adding 1 day should add exactly 24 hours in UTC
77
+ expect(parseFlexibleTime("1d", beforeDST)).toBe("2024-03-10T12:00:00.000Z");
78
+ expect(parseFlexibleTime("1d", afterDST)).toBe("2024-03-12T12:00:00.000Z");
79
+
80
+ // Week start should still be Monday 00:00:00 UTC
81
+ expect(parseFlexibleTime("week-start", beforeDST)).toBe("2024-03-04T00:00:00.000Z");
82
+ expect(parseFlexibleTime("week-start", afterDST)).toBe("2024-03-11T00:00:00.000Z");
83
+ });
84
+
85
+ it("should handle year boundary crossing in UTC", () => {
86
+ const endOfYear = new Date("2024-12-31T23:00:00.000Z");
87
+
88
+ expect(parseFlexibleTime("1d", endOfYear)).toBe("2025-01-01T23:00:00.000Z");
89
+ expect(parseFlexibleTime("1w", endOfYear)).toBe("2025-01-07T23:00:00.000Z");
90
+ expect(parseFlexibleTime("today", endOfYear)).toBe("2024-12-31T00:00:00.000Z");
91
+ });
92
+
93
+ it("should handle leap year correctly in UTC", () => {
94
+ const leapDay = new Date("2024-02-29T12:00:00.000Z");
95
+
96
+ expect(parseFlexibleTime("1d", leapDay)).toBe("2024-03-01T12:00:00.000Z");
97
+ expect(parseFlexibleTime("-1d", leapDay)).toBe("2024-02-28T12:00:00.000Z");
98
+ expect(parseFlexibleTime("today", leapDay)).toBe("2024-02-29T00:00:00.000Z");
99
+ });
100
+ });
101
+
102
+ describe("relative time parser", () => {
103
+ it("should subtract time in UTC", () => {
104
+ vi.setSystemTime(new Date("2024-07-26T15:20:00.000Z"));
105
+
106
+ // All subtractions should be exact in UTC
107
+ expect(parseRelativeTime("1h")).toBe("2024-07-26T14:20:00.000Z");
108
+ expect(parseRelativeTime("1d")).toBe("2024-07-25T15:20:00.000Z");
109
+ expect(parseRelativeTime("1w")).toBe("2024-07-19T15:20:00.000Z");
110
+ expect(parseRelativeTime("1M")).toBe("2024-06-26T15:20:00.000Z");
111
+ });
112
+
113
+ it("should handle DST boundaries when subtracting time", () => {
114
+ // Set time after DST transition
115
+ vi.setSystemTime(new Date("2024-03-11T15:20:00.000Z"));
116
+
117
+ // Subtracting 1 day should go back exactly 24 hours in UTC
118
+ expect(parseRelativeTime("1d")).toBe("2024-03-10T15:20:00.000Z");
119
+ expect(parseRelativeTime("2d")).toBe("2024-03-09T15:20:00.000Z");
120
+ });
121
+ });
122
+
123
+ describe("cross-timezone consistency", () => {
124
+ it("should produce identical results for UTC date components", () => {
125
+ // All these operations should produce results with consistent UTC components
126
+ const results = [
127
+ parseFlexibleTime("today", baseTime),
128
+ parseFlexibleTime("week-start", baseTime),
129
+ parseFlexibleTime("week-end", baseTime),
130
+ ];
131
+
132
+ for (const result of results) {
133
+ const date = new Date(result);
134
+ // All results should end with Z (UTC)
135
+ expect(result).toMatch(/Z$/);
136
+ // Date parsing should work correctly
137
+ expect(date.toISOString()).toBe(result);
138
+ }
139
+ });
140
+
141
+ it("should handle midnight edge cases in UTC", () => {
142
+ const midnight = new Date("2024-01-15T00:00:00.000Z");
143
+ const almostMidnight = new Date("2024-01-14T23:59:59.999Z");
144
+ const afterMidnight = new Date("2024-01-15T00:00:00.001Z");
145
+
146
+ // All should normalize to the same day start
147
+ expect(parseFlexibleTime("today", midnight)).toBe("2024-01-15T00:00:00.000Z");
148
+ expect(parseFlexibleTime("today", almostMidnight)).toBe("2024-01-14T00:00:00.000Z");
149
+ expect(parseFlexibleTime("today", afterMidnight)).toBe("2024-01-15T00:00:00.000Z");
150
+ });
151
+
152
+ it("should maintain precision to milliseconds in UTC", () => {
153
+ const preciseTime = new Date("2024-01-15T12:34:56.789Z");
154
+
155
+ // Week boundaries should maintain millisecond precision
156
+ const weekEnd = parseFlexibleTime("week-end", preciseTime);
157
+ expect(weekEnd).toBe("2024-01-21T23:59:59.999Z");
158
+ expect(weekEnd.endsWith(".999Z")).toBe(true);
159
+
160
+ // 'now' should preserve exact time
161
+ expect(parseFlexibleTime("now", preciseTime)).toBe("2024-01-15T12:34:56.789Z");
162
+ });
163
+ });
164
+
165
+ describe("ISO 8601 preservation", () => {
166
+ it("should preserve .000Z format in all outputs", () => {
167
+ const results = [
168
+ parseFlexibleTime("today", baseTime),
169
+ parseFlexibleTime("now", baseTime),
170
+ parseFlexibleTime("1d", baseTime),
171
+ parseFlexibleTime("week-start", baseTime),
172
+ ];
173
+
174
+ for (const result of results) {
175
+ expect(result).toMatch(/\.\d{3}Z$/);
176
+ }
177
+ });
178
+
179
+ it("should handle timezone offset inputs correctly", () => {
180
+ // Input with timezone offset should normalize to UTC
181
+ const withOffset = "2024-01-15T13:00:00+01:00";
182
+ const result = parseFlexibleTime(withOffset, baseTime);
183
+
184
+ // Should normalize to UTC (13:00+01:00 = 12:00Z)
185
+ expect(result).toBe("2024-01-15T12:00:00.000Z");
186
+ });
187
+ });
188
+ });