@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,36 @@
1
+ import { getLogger } from "@schafevormfenster/logging";
2
+ import dayjs from "dayjs";
3
+ import utc from "dayjs/plugin/utc.js";
4
+ dayjs.extend(utc);
5
+ const log = getLogger("rest.helpers.relative-time-parser");
6
+ /**
7
+ * Helper function to convert relative time expressions to ISO8601 timestamps
8
+ * All calculations are performed in UTC timezone
9
+ *
10
+ * @param relativeTime - Relative time expression like "1d", "3h", "30m"
11
+ * @returns ISO8601 timestamp string
12
+ */
13
+ export const parseRelativeTime = (relativeTime) => {
14
+ const relativePattern = /^(\d+)([mhdwM])$/;
15
+ const match = relativePattern.exec(relativeTime);
16
+ if (!match) {
17
+ log.error({ relativeTime, error: `Invalid relative time format: ${relativeTime}` }, `Invalid relative time format: ${relativeTime}`);
18
+ throw new Error(`Invalid relative time format: ${relativeTime}`);
19
+ }
20
+ const [, amount, unit] = match;
21
+ const numberAmount = Number.parseInt(amount, 10);
22
+ // Map unit to Day.js unit names
23
+ const unitMap = {
24
+ m: "minute",
25
+ h: "hour",
26
+ d: "day",
27
+ w: "week",
28
+ M: "month",
29
+ };
30
+ const dayjsUnit = unitMap[unit];
31
+ if (!dayjsUnit) {
32
+ log.error({ unit, error: `Invalid time unit: ${unit}` }, `Invalid time unit: ${unit}`);
33
+ throw new Error(`Invalid time unit: ${unit}`);
34
+ }
35
+ return dayjs.utc().subtract(numberAmount, dayjsUnit).toISOString();
36
+ };
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Core validation schema for relative time expressions
4
+ * Supports: "1m" (minute), "2h" (hour), "3d" (day), "1w" (week), "2M" (month)
5
+ * Validates format but does not transform
6
+ */
7
+ export declare const RelativeTimeSchema: z.ZodString;
8
+ export type RelativeTime = z.infer<typeof RelativeTimeSchema>;
9
+ /**
10
+ * Transformation schema that converts relative time to ISO8601
11
+ * Direction: past (subtracts from current time)
12
+ * @example
13
+ * RelativeTimePastSchema.parse("1d") // "2024-01-14T12:00:00.000Z" (if now is 2024-01-15T12:00:00Z)
14
+ */
15
+ export declare const RelativeTimePastSchema: z.ZodEffects<z.ZodString, string, string>;
16
+ /**
17
+ * Transformation schema that converts relative time to ISO8601
18
+ * Direction: future (adds to current time)
19
+ * @example
20
+ * RelativeTimeFutureSchema.parse("1d") // "2024-01-16T12:00:00.000Z" (if now is 2024-01-15T12:00:00Z)
21
+ */
22
+ export declare const RelativeTimeFutureSchema: z.ZodEffects<z.ZodString, string, string>;
23
+ //# sourceMappingURL=relative-time.schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relative-time.schema.d.ts","sourceRoot":"","sources":["../../src/time/relative-time.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,aAM5B,CAAC;AAEJ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,2CAElC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,2CAEpC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { parseRelativeTime } from "./time-helpers";
3
+ /**
4
+ * Core validation schema for relative time expressions
5
+ * Supports: "1m" (minute), "2h" (hour), "3d" (day), "1w" (week), "2M" (month)
6
+ * Validates format but does not transform
7
+ */
8
+ export const RelativeTimeSchema = z
9
+ .string()
10
+ .describe("Relative time expression")
11
+ .regex(/^(\d+)([mhdwM])$/, "Must be a relative time expression (e.g., '1d', '3h', '30m', '1w', '2M')");
12
+ /**
13
+ * Transformation schema that converts relative time to ISO8601
14
+ * Direction: past (subtracts from current time)
15
+ * @example
16
+ * RelativeTimePastSchema.parse("1d") // "2024-01-14T12:00:00.000Z" (if now is 2024-01-15T12:00:00Z)
17
+ */
18
+ export const RelativeTimePastSchema = RelativeTimeSchema.transform((value) => parseRelativeTime(value, new Date(), "past"));
19
+ /**
20
+ * Transformation schema that converts relative time to ISO8601
21
+ * Direction: future (adds to current time)
22
+ * @example
23
+ * RelativeTimeFutureSchema.parse("1d") // "2024-01-16T12:00:00.000Z" (if now is 2024-01-15T12:00:00Z)
24
+ */
25
+ export const RelativeTimeFutureSchema = RelativeTimeSchema.transform((value) => parseRelativeTime(value, new Date(), "future"));
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema that accepts either ISO8601 timestamps or relative time expressions
4
+ * All calculations are performed in UTC timezone
5
+ */
6
+ export declare const SinceParameterSchema: z.ZodEffects<z.ZodString, string, string>;
7
+ export type SinceParameter = z.infer<typeof SinceParameterSchema>;
8
+ //# sourceMappingURL=since-parameter.schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"since-parameter.schema.d.ts","sourceRoot":"","sources":["../../src/time/since-parameter.schema.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;GAGG;AACH,eAAO,MAAM,oBAAoB,2CAwD7B,CAAC;AAEL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -0,0 +1,56 @@
1
+ import dayjs from "dayjs";
2
+ import utc from "dayjs/plugin/utc.js";
3
+ import { z } from "zod";
4
+ dayjs.extend(utc);
5
+ /**
6
+ * Schema that accepts either ISO8601 timestamps or relative time expressions
7
+ * All calculations are performed in UTC timezone
8
+ */
9
+ export const SinceParameterSchema = z
10
+ .string()
11
+ .describe("Timestamp in ISO8601 format or relative time expression")
12
+ .transform((value, context) => {
13
+ // Try to parse as ISO8601 first
14
+ const iso8601Pattern = /^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$/;
15
+ if (iso8601Pattern.test(value)) {
16
+ try {
17
+ return new Date(value).toISOString();
18
+ }
19
+ catch {
20
+ context.addIssue({
21
+ code: z.ZodIssueCode.custom,
22
+ message: "Invalid ISO8601 timestamp format",
23
+ });
24
+ return z.NEVER;
25
+ }
26
+ }
27
+ // Try to parse as relative time
28
+ const relativePattern = /^(\d+)([mhdwM])$/;
29
+ const match = relativePattern.exec(value);
30
+ if (!match) {
31
+ context.addIssue({
32
+ code: z.ZodIssueCode.custom,
33
+ message: "Must be either ISO8601 timestamp or relative time expression (e.g., '1d', '3h', '30m')",
34
+ });
35
+ return z.NEVER;
36
+ }
37
+ const [, amount, unit] = match;
38
+ const numberAmount = Number.parseInt(amount, 10);
39
+ // Map unit to Day.js unit names
40
+ const unitMap = {
41
+ m: "minute",
42
+ h: "hour",
43
+ d: "day",
44
+ w: "week",
45
+ M: "month",
46
+ };
47
+ const dayjsUnit = unitMap[unit];
48
+ if (!dayjsUnit) {
49
+ context.addIssue({
50
+ code: z.ZodIssueCode.custom,
51
+ message: "Invalid time unit. Use 'm' (minutes), 'h' (hours), 'd' (days), 'w' (weeks), or 'M' (months)",
52
+ });
53
+ return z.NEVER;
54
+ }
55
+ return dayjs.utc().subtract(numberAmount, dayjsUnit).toISOString();
56
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Helper function to parse relative time and calculate the datetime
3
+ * All calculations are performed in UTC timezone
4
+ * @param relativeTime - Relative time string like "1d", "3h", "30m"
5
+ * @param from - Base datetime to calculate from (defaults to now)
6
+ * @param direction - Whether to subtract (past) or add (future)
7
+ * @returns ISO8601 datetime string
8
+ */
9
+ export declare function parseRelativeTime(relativeTime: string, from?: Date, direction?: "past" | "future"): string;
10
+ /**
11
+ * Helper function to parse flexible time (ISO8601 or relative) to ISO8601
12
+ * All calculations are performed in UTC timezone
13
+ * @param flexibleTime - ISO8601 string or relative time expression
14
+ * @param from - Base datetime for relative time calculations (defaults to now)
15
+ * @param direction - Whether relative times are past or future (defaults to past)
16
+ * @returns ISO8601 datetime string
17
+ */
18
+ export declare function parseFlexibleTime(flexibleTime: string, from?: Date, direction?: "past" | "future"): string;
19
+ //# sourceMappingURL=time-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time-helpers.d.ts","sourceRoot":"","sources":["../../src/time/time-helpers.ts"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,IAAI,GAAE,IAAiB,EACvB,SAAS,GAAE,MAAM,GAAG,QAAiB,GACpC,MAAM,CA2BR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,IAAI,GAAE,IAAiB,EACvB,SAAS,GAAE,MAAM,GAAG,QAAiB,GACpC,MAAM,CAgBR"}
@@ -0,0 +1,56 @@
1
+ import dayjs from "dayjs";
2
+ import utc from "dayjs/plugin/utc.js";
3
+ import { ISO8601Schema } from "./iso8601.schema";
4
+ import { RelativeTimeSchema } from "./relative-time.schema";
5
+ dayjs.extend(utc);
6
+ /**
7
+ * Helper function to parse relative time and calculate the datetime
8
+ * All calculations are performed in UTC timezone
9
+ * @param relativeTime - Relative time string like "1d", "3h", "30m"
10
+ * @param from - Base datetime to calculate from (defaults to now)
11
+ * @param direction - Whether to subtract (past) or add (future)
12
+ * @returns ISO8601 datetime string
13
+ */
14
+ export function parseRelativeTime(relativeTime, from = new Date(), direction = "past") {
15
+ const match = relativeTime.match(/^(\d+)([mhdwM])$/);
16
+ if (!match) {
17
+ throw new Error(`Invalid relative time format: ${relativeTime}. Expected format like '1d', '3h', '30m'`);
18
+ }
19
+ const [, amount, unit] = match;
20
+ const numberAmount = Number.parseInt(amount, 10);
21
+ // Map unit to Day.js unit names
22
+ const unitMap = {
23
+ m: "minute",
24
+ h: "hour",
25
+ d: "day",
26
+ w: "week",
27
+ M: "month",
28
+ };
29
+ const dayjsUnit = unitMap[unit];
30
+ if (!dayjsUnit) {
31
+ throw new Error(`Invalid time unit: ${unit}`);
32
+ }
33
+ const operation = direction === "past" ? "subtract" : "add";
34
+ return dayjs.utc(from)[operation](numberAmount, dayjsUnit).toISOString();
35
+ }
36
+ /**
37
+ * Helper function to parse flexible time (ISO8601 or relative) to ISO8601
38
+ * All calculations are performed in UTC timezone
39
+ * @param flexibleTime - ISO8601 string or relative time expression
40
+ * @param from - Base datetime for relative time calculations (defaults to now)
41
+ * @param direction - Whether relative times are past or future (defaults to past)
42
+ * @returns ISO8601 datetime string
43
+ */
44
+ export function parseFlexibleTime(flexibleTime, from = new Date(), direction = "past") {
45
+ // Try to parse as ISO8601 first
46
+ const isoResult = ISO8601Schema.safeParse(flexibleTime);
47
+ if (isoResult.success) {
48
+ return new Date(flexibleTime).toISOString();
49
+ }
50
+ // Try to parse as relative time
51
+ const relativeResult = RelativeTimeSchema.safeParse(flexibleTime);
52
+ if (relativeResult.success) {
53
+ return parseRelativeTime(flexibleTime, from, direction);
54
+ }
55
+ throw new Error(`Invalid flexible time format: ${flexibleTime}. Expected ISO8601 datetime or relative time expression`);
56
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export type TimeUnit = "s" | "m" | "h" | "d" | "w";
3
+ export interface RelativeTimeConfig {
4
+ amount: number;
5
+ unit: TimeUnit;
6
+ }
7
+ export declare const RelativeTimeSchema: z.ZodUnion<[z.ZodLiteral<"now">, z.ZodLiteral<"today">, z.ZodLiteral<"tomorrow">, z.ZodLiteral<"yesterday">, z.ZodString, z.ZodString]>;
8
+ /**
9
+ * Flexible time schema that accepts either ISO 8601 dates or relative time formats
10
+ *
11
+ * Supports:
12
+ * - ISO 8601 dates: "2024-01-01T12:00:00Z"
13
+ * - Literals: "now" (current datetime), "today" (current day at 00:00:00), "tomorrow" (next day), "yesterday" (previous day)
14
+ * - Relative future times: "30s", "5m", "2h", "3d", "1w" (positive = future)
15
+ * - Relative past times: "-1d" (yesterday), "-2w" (2 weeks ago) (negative = past)
16
+ * - 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)
17
+ */
18
+ export declare const FlexibleTimeSchema: z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodUnion<[z.ZodLiteral<"now">, z.ZodLiteral<"today">, z.ZodLiteral<"tomorrow">, z.ZodLiteral<"yesterday">, z.ZodString, z.ZodString]>]>;
19
+ export type FlexibleTime = z.infer<typeof FlexibleTimeSchema>;
20
+ //# sourceMappingURL=time-schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time-schemas.d.ts","sourceRoot":"","sources":["../../src/time/time-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,MAAM,MAAM,QAAQ,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEnD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,yIAiB7B,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,kMAA+C,CAAC;AAC/E,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { ISO8601Schema } from "./iso8601.types";
3
+ export const RelativeTimeSchema = z.union([
4
+ z.literal("now"),
5
+ z.literal("today"),
6
+ z.literal("tomorrow"),
7
+ z.literal("yesterday"),
8
+ z
9
+ .string()
10
+ .regex(/^(-?\d+)([smhdw])$/, "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)"),
11
+ z
12
+ .string()
13
+ .regex(/^week-(start|end)(([+-])\d+w)?$/, "Invalid week notation format. Use format like 'week-start', 'week-end', 'week-start+1w', 'week-end-2w'"),
14
+ ]);
15
+ /**
16
+ * Flexible time schema that accepts either ISO 8601 dates or relative time formats
17
+ *
18
+ * Supports:
19
+ * - ISO 8601 dates: "2024-01-01T12:00:00Z"
20
+ * - Literals: "now" (current datetime), "today" (current day at 00:00:00), "tomorrow" (next day), "yesterday" (previous day)
21
+ * - Relative future times: "30s", "5m", "2h", "3d", "1w" (positive = future)
22
+ * - Relative past times: "-1d" (yesterday), "-2w" (2 weeks ago) (negative = past)
23
+ * - 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)
24
+ */
25
+ export const FlexibleTimeSchema = z.union([ISO8601Schema, RelativeTimeSchema]);
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich"
4
+ */
5
+ export declare const IanaTimezone: z.ZodString;
6
+ /**
7
+ * Common time standards like UTC, GMT, etc.
8
+ */
9
+ export declare const TimeStandard: z.ZodString;
10
+ /**
11
+ * Timezone that can be either an IANA timezone or a time standard
12
+ */
13
+ export declare const Timezone: z.ZodUnion<[z.ZodString, z.ZodString]>;
14
+ export type IanaTimezone = z.infer<typeof IanaTimezone>;
15
+ export type TimeStandard = z.infer<typeof TimeStandard>;
16
+ export type Timezone = z.infer<typeof Timezone>;
17
+ //# sourceMappingURL=timezone.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone.types.d.ts","sourceRoot":"","sources":["../../src/time/timezone.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,YAAY,aAA+C,CAAC;AAEzE;;GAEG;AACH,eAAO,MAAM,YAAY,aAE8B,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,QAAQ,wCAAwC,CAAC;AAE9D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AACxD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AACxD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich"
4
+ */
5
+ export const IanaTimezone = z.string().regex(/^[A-Za-z_]+\/[A-Za-z_]+$/);
6
+ /**
7
+ * Common time standards like UTC, GMT, etc.
8
+ */
9
+ export const TimeStandard = z
10
+ .string()
11
+ .regex(/^(UTC|GMT|EST|CST|MST|PST|EDT|CDT|MDT|PDT)$/);
12
+ /**
13
+ * Timezone that can be either an IANA timezone or a time standard
14
+ */
15
+ export const Timezone = z.union([IanaTimezone, TimeStandard]);
@@ -0,0 +1,3 @@
1
+ import { TsRestResponse } from "@ts-rest/serverless";
2
+ export declare function handleZodError(error: unknown): Promise<TsRestResponse>;
3
+ //# sourceMappingURL=zod-error-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-error-handler.d.ts","sourceRoot":"","sources":["../../src/validation/zod-error-handler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA0GrD,wBAAsB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAqI5E"}
@@ -0,0 +1,189 @@
1
+ import { getLogger } from "@schafevormfenster/logging";
2
+ import { TsRestResponse } from "@ts-rest/serverless";
3
+ import { ZodError } from "zod";
4
+ const log = getLogger("api.validation.zod");
5
+ /**
6
+ * Extract a meaningful error summary from ZodError
7
+ * Simplified version to reduce cognitive complexity
8
+ */
9
+ function getZodErrorSummary(error) {
10
+ if (!error.issues || error.issues.length === 0) {
11
+ return "Validation error: Validation failed";
12
+ }
13
+ // Check for union errors
14
+ const unionIssue = error.issues.find((issue) => issue.code === "invalid_union" &&
15
+ "unionErrors" in issue &&
16
+ Array.isArray(issue.unionErrors));
17
+ if (unionIssue && "unionErrors" in unionIssue) {
18
+ const errors = extractUnionErrors(unionIssue.unionErrors);
19
+ if (errors.length > 0) {
20
+ return formatErrorSummary(errors.slice(0, 3).join("; "));
21
+ }
22
+ }
23
+ // Fallback to first issue
24
+ const firstIssue = error.issues[0];
25
+ const path = firstIssue.path.length > 0 ? firstIssue.path.join(".") : "input";
26
+ return formatErrorSummary(`${firstIssue.message} at ${path}`);
27
+ }
28
+ /**
29
+ * Extract non-discriminator errors from union branches
30
+ * Simplified to reduce cognitive complexity
31
+ */
32
+ function extractUnionErrors(unionErrors) {
33
+ const errors = [];
34
+ for (const unionError of unionErrors) {
35
+ if (!(unionError instanceof ZodError) || unionError.issues.length === 0) {
36
+ continue;
37
+ }
38
+ const relevantIssues = unionError.issues.filter((subIssue) => subIssue.code !== "invalid_literal" &&
39
+ subIssue.code !== "invalid_union");
40
+ for (const subIssue of relevantIssues) {
41
+ const path = subIssue.path.length > 0 ? subIssue.path.join(".") : "input";
42
+ errors.push(`${subIssue.message} (${path})`);
43
+ }
44
+ }
45
+ return errors;
46
+ }
47
+ /**
48
+ * Format error summary with consistent prefix
49
+ */
50
+ function formatErrorSummary(summary) {
51
+ return summary.toLowerCase().startsWith("validation error")
52
+ ? summary
53
+ : `Validation error: ${summary}`;
54
+ }
55
+ /**
56
+ * Check if error is likely a response validation error (from ts-rest)
57
+ * Response validation errors typically have statusCode property set by ts-rest
58
+ */
59
+ function isResponseValidationError(error) {
60
+ return (typeof error === "object" &&
61
+ error !== null &&
62
+ "statusCode" in error &&
63
+ error.statusCode === 500);
64
+ }
65
+ /**
66
+ * Safely extract and truncate request body for logging
67
+ * Following defensive logging guidelines - never assume payload structure
68
+ */
69
+ function getSafePayloadExcerpt(payload) {
70
+ if (!payload) {
71
+ return undefined;
72
+ }
73
+ try {
74
+ const jsonString = JSON.stringify(payload);
75
+ // Truncate to 500 characters to avoid excessive log size
76
+ return jsonString.slice(0, 500);
77
+ }
78
+ catch {
79
+ // If JSON.stringify fails (circular references, etc.), fall back to type check
80
+ return `[Unable to serialize: ${typeof payload}]`;
81
+ }
82
+ }
83
+ export async function handleZodError(error) {
84
+ // JSON parsing errors (SyntaxError from malformed JSON)
85
+ if (error instanceof SyntaxError) {
86
+ const jsonError = {
87
+ status: 400,
88
+ error: `Invalid JSON: ${error.message}`,
89
+ trace: error,
90
+ };
91
+ log.error({ error: jsonError }, "JSON parsing error occurred");
92
+ return new TsRestResponse(JSON.stringify(jsonError), {
93
+ status: 400,
94
+ headers: {
95
+ "Content-Type": "application/json",
96
+ },
97
+ });
98
+ }
99
+ // Input validation errors (ZodError from request validation)
100
+ if (error instanceof ZodError) {
101
+ const errorSummary = getZodErrorSummary(error);
102
+ const zodError = {
103
+ status: 400,
104
+ error: errorSummary,
105
+ trace: error,
106
+ };
107
+ // Note: Direct ZodError typically doesn't have request body access
108
+ // Request body is available in ts-rest wrapper errors (handled below)
109
+ log.error({ error: zodError }, "Input validation error occurred");
110
+ return new TsRestResponse(JSON.stringify(zodError), {
111
+ status: 400,
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ },
115
+ });
116
+ }
117
+ // Response validation errors (statusCode 500 from ts-rest)
118
+ if (isResponseValidationError(error)) {
119
+ const errorObject = error;
120
+ let errorMessage = "Response validation failed";
121
+ // Extract meaningful error from ZodError cause if available
122
+ if (errorObject.cause instanceof ZodError) {
123
+ errorMessage = `Response validation failed: ${getZodErrorSummary(errorObject.cause)}`;
124
+ }
125
+ const responseError = {
126
+ status: 500,
127
+ error: errorMessage,
128
+ trace: error,
129
+ };
130
+ log.error({ error: responseError }, "Response validation error occurred");
131
+ return new TsRestResponse(JSON.stringify(responseError), {
132
+ status: 500,
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ },
136
+ });
137
+ }
138
+ // Input validation errors with statusCode 400 (from ts-rest request validation)
139
+ if (typeof error === "object" &&
140
+ error !== null &&
141
+ "statusCode" in error &&
142
+ error.statusCode === 400) {
143
+ const errorObject = error;
144
+ let errorMessage = "Validation Error";
145
+ // Extract meaningful error from ZodError cause or bodyError if available
146
+ if (errorObject.cause instanceof ZodError) {
147
+ errorMessage = getZodErrorSummary(errorObject.cause);
148
+ }
149
+ else if (errorObject.bodyError instanceof ZodError) {
150
+ errorMessage = getZodErrorSummary(errorObject.bodyError);
151
+ }
152
+ // Extract request body for debugging if available
153
+ const requestBody = errorObject.body && "body" in errorObject.body ? errorObject.body.body : undefined;
154
+ // Get safe truncated payload excerpt for logging
155
+ const payloadExcerpt = getSafePayloadExcerpt(requestBody);
156
+ const zodError = {
157
+ status: 400,
158
+ error: errorMessage,
159
+ trace: {
160
+ ...error,
161
+ requestBody, // Include the actual request body for debugging
162
+ },
163
+ };
164
+ // Enhanced logging with sanitized payload excerpt
165
+ log.error({
166
+ error: zodError,
167
+ payloadExcerpt, // Truncated, safe payload snapshot
168
+ }, "Input validation error occurred");
169
+ return new TsRestResponse(JSON.stringify(zodError), {
170
+ status: 400,
171
+ headers: {
172
+ "Content-Type": "application/json",
173
+ },
174
+ });
175
+ }
176
+ // Unknown errors
177
+ const unknownError = {
178
+ status: 500,
179
+ error: "Internal Server Error",
180
+ trace: error,
181
+ };
182
+ log.error({ error: unknownError }, "An unknown error occurred");
183
+ return new TsRestResponse(JSON.stringify(unknownError), {
184
+ status: 500,
185
+ headers: {
186
+ "Content-Type": "application/json",
187
+ },
188
+ });
189
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Extracts a clean, user-friendly error message from Zod validation errors.
3
+ * Returns the first validation error message instead of the full error object.
4
+ *
5
+ * @param error - The error from Zod validation
6
+ * @returns Clean error message string
7
+ */
8
+ export declare function extractZodErrorMessage(error: unknown): string;
9
+ //# sourceMappingURL=zod-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-utils.d.ts","sourceRoot":"","sources":["../../src/validation/zod-utils.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAgB7D"}
@@ -0,0 +1,23 @@
1
+ import { ZodError } from "zod";
2
+ /**
3
+ * Extracts a clean, user-friendly error message from Zod validation errors.
4
+ * Returns the first validation error message instead of the full error object.
5
+ *
6
+ * @param error - The error from Zod validation
7
+ * @returns Clean error message string
8
+ */
9
+ export function extractZodErrorMessage(error) {
10
+ if (error instanceof ZodError) {
11
+ // Get the first error message from Zod validation
12
+ const firstError = error.errors[0];
13
+ if (firstError?.message && firstError.message.trim() !== "") {
14
+ return firstError.message;
15
+ }
16
+ // If ZodError has no useful message, return default instead of falling through to Error branch
17
+ return "Schema validation failed";
18
+ }
19
+ if (error instanceof Error) {
20
+ return error.message;
21
+ }
22
+ return "Schema validation failed";
23
+ }
@@ -0,0 +1,16 @@
1
+ import baseConfig from "@schafevormfenster/eslint-config";
2
+ import vitestConfig from "@schafevormfenster/eslint-config/vitest";
3
+
4
+ export default [
5
+ ...baseConfig,
6
+ ...vitestConfig,
7
+ {
8
+ ignores: ["dist/**", "node_modules/**", "bin/**"],
9
+ },
10
+ {
11
+ rules: {
12
+ "import/no-unresolved": "off", // TypeScript handles this
13
+ "import/named": "off", // TypeScript handles type exports
14
+ },
15
+ },
16
+ ];
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@schafevormfenster/rest-commons",
3
+ "version": "0.1.1",
4
+ "description": "Centralized authority for REST standards and schemas - XSD schemas, parsing functions, and coding instructions",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "setup-rest-commons": "./bin/setup.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=24"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.19.3",
22
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
23
+ "@typescript-eslint/parser": "^8.51.0",
24
+ "eslint": "^9.39.2",
25
+ "typescript": "^5.9.3",
26
+ "vitest": "^4.0.16",
27
+ "@schafevormfenster/eslint-config": "0.0.7"
28
+ },
29
+ "dependencies": {
30
+ "dayjs": "^1.11.19",
31
+ "slugify": "^1.6.6",
32
+ "zod": "^3.25.76",
33
+ "@schafevormfenster/logging": "0.1.0",
34
+ "@schafevormfenster/security": "0.1.0"
35
+ },
36
+ "scripts": {
37
+ "typecheck": "tsc --noEmit",
38
+ "lint": "eslint",
39
+ "test": "TZ=UTC vitest run",
40
+ "test:watch": "TZ=UTC vitest",
41
+ "build": "tsc",
42
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build"
43
+ }
44
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { ApiErrorSchema, ApiErrorConstructor } from "./error.schema";
4
+
5
+ describe("ApiErrorSchema", () => {
6
+ it("validates error shape for 4xx/5xx", () => {
7
+ const parsed = ApiErrorSchema.parse({ status: 404, error: "Not Found" });
8
+ expect(parsed.status).toBe(404);
9
+ expect(parsed.error).toMatch(/not found/i);
10
+ });
11
+
12
+ it("rejects non-error status codes", () => {
13
+ expect(() => ApiErrorSchema.parse({ status: 200, error: "ok" })).toThrow();
14
+ expect(() =>
15
+ ApiErrorSchema.parse({ status: 600, error: "invalid" })
16
+ ).toThrow();
17
+ });
18
+ });
19
+
20
+ describe("ApiError class", () => {
21
+ it("sets status and name", () => {
22
+ const error = new ApiErrorConstructor(418, "I'm a teapot");
23
+ expect(error.status).toBe(418);
24
+ expect(error.message).toMatch(/i'm a teapot/i);
25
+ expect(error.name).toBe("ApiError");
26
+ });
27
+ });