@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,23 @@
1
+ import { z } from "zod";
2
+
3
+ export const ApiErrorSchema = z
4
+ .object({
5
+ status: z.number().min(400).max(599),
6
+ error: z.string(),
7
+ trace: z.any().optional(),
8
+ })
9
+ .strict();
10
+
11
+ // Type export matching schema name (for @schafevormfenster/enforce-schema-type rule)
12
+ export type ApiError = z.infer<typeof ApiErrorSchema>;
13
+
14
+ // Class for error handling with instanceof checks and throwing
15
+ export class ApiErrorConstructor extends Error {
16
+ readonly status: number;
17
+
18
+ constructor(status: number, message: string) {
19
+ super(message);
20
+ this.status = status;
21
+ this.name = "ApiError";
22
+ }
23
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import {
4
+ ApiInfoSchema,
5
+ ApiStatusSchema,
6
+ HealthyApiStatusSchema,
7
+ HealthyServiceInfoSchema,
8
+ ServiceStatusSchema,
9
+ UnhealthyApiStatusSchema,
10
+ UnhealthyServiceInfoSchema,
11
+ } from "./health.schema";
12
+
13
+ describe("health.schema", () => {
14
+ it("parses healthy service info", () => {
15
+ const service = HealthyServiceInfoSchema.parse({
16
+ name: "Example",
17
+ status: 200,
18
+ message: "healthy",
19
+ version: "1.0.0",
20
+ });
21
+ expect(service.name).toBe("Example");
22
+ expect(service.status).toBe(200);
23
+ expect(service.message).toMatch(/healthy/i);
24
+ });
25
+
26
+ it("parses unhealthy service info", () => {
27
+ const service = UnhealthyServiceInfoSchema.parse({
28
+ name: "Example",
29
+ status: 503,
30
+ error: "Service unavailable",
31
+ version: "1.0.0",
32
+ });
33
+ expect(service.status).toBe(503);
34
+ expect(service.error).toMatch(/service unavailable/i);
35
+ });
36
+
37
+ it("parses healthy API status", () => {
38
+ const status = HealthyApiStatusSchema.parse({
39
+ status: 200,
40
+ name: "api",
41
+ version: "1.0.0",
42
+ description: "ok",
43
+ services: [
44
+ { name: "svc", status: 200, message: "healthy" },
45
+ { name: "svc2", status: 500, error: "fail" },
46
+ ],
47
+ });
48
+ expect(status.status).toBe(200);
49
+ expect(status.services).toHaveLength(2);
50
+ });
51
+
52
+ it("parses unhealthy API status", () => {
53
+ const status = UnhealthyApiStatusSchema.parse({
54
+ status: 500,
55
+ error: "bad",
56
+ name: "api",
57
+ version: "1.0.0",
58
+ services: [{ name: "svc", status: 500, error: "boom" }],
59
+ });
60
+ expect(status.error).toMatch(/bad/i);
61
+ });
62
+
63
+ it("ApiInfoSchema validates basic api info", () => {
64
+ const info = ApiInfoSchema.parse({
65
+ name: "api",
66
+ version: "1.2.3",
67
+ description: "desc",
68
+ services: [{ name: "svc", status: 200, message: "healthy" }],
69
+ });
70
+ expect(info.name).toBe("api");
71
+ });
72
+
73
+ it("ApiStatusSchema accepts both healthy and unhealthy variants", () => {
74
+ const healthy = ApiStatusSchema.parse({
75
+ status: 200,
76
+ name: "api",
77
+ version: "1.0.0",
78
+ services: [{ name: "svc", status: 200, message: "healthy" }],
79
+ });
80
+ expect(healthy.status).toBe(200);
81
+
82
+ const unhealthy = ApiStatusSchema.parse({
83
+ status: 503,
84
+ error: "down",
85
+ name: "api",
86
+ version: "1.0.0",
87
+ services: [{ name: "svc", status: 503, error: "down" }],
88
+ });
89
+ expect(unhealthy.status).toBe(503);
90
+ });
91
+
92
+ it("ServiceStatusSchema union validates both variants", () => {
93
+ expect(
94
+ ServiceStatusSchema.parse({
95
+ name: "svc",
96
+ status: 200,
97
+ message: "healthy",
98
+ })
99
+ ).toBeTruthy();
100
+ expect(
101
+ ServiceStatusSchema.parse({ name: "svc", status: 500, error: "err" })
102
+ ).toBeTruthy();
103
+ });
104
+ });
@@ -0,0 +1,63 @@
1
+
2
+ import { z } from "zod";
3
+
4
+ import { ApiErrorSchema } from "./error.schema";
5
+ import { OkaySchema } from "./okay.schema";
6
+
7
+ /**
8
+ * Services
9
+ */
10
+
11
+ export const ServiceInfoSchema = z.object({
12
+ name: z.string(),
13
+ version: z.string().optional(),
14
+ }).strict();
15
+
16
+ export type ServiceInfo = z.infer<typeof ServiceInfoSchema>;
17
+
18
+ export const HealthyServiceInfoSchema = OkaySchema.merge(
19
+ ServiceInfoSchema
20
+ ).extend({
21
+ message: z.literal("healthy"),
22
+ });
23
+
24
+ export type HealthyServiceInfo = z.infer<typeof HealthyServiceInfoSchema>;
25
+
26
+ export const UnhealthyServiceInfoSchema =
27
+ ApiErrorSchema.merge(ServiceInfoSchema);
28
+
29
+ export type UnhealthyServiceInfo = z.infer<
30
+ typeof UnhealthyServiceInfoSchema
31
+ >;
32
+
33
+ export const ServiceStatusSchema = z.union([
34
+ HealthyServiceInfoSchema,
35
+ UnhealthyServiceInfoSchema,
36
+ ]);
37
+
38
+ export type ServiceStatus = z.infer<typeof ServiceStatusSchema>;
39
+
40
+ /**
41
+ * API
42
+ */
43
+ export const ApiInfoSchema = z.object({
44
+ name: z.string(),
45
+ version: z.string(),
46
+ description: z.string().optional(),
47
+ services: ServiceStatusSchema.array(),
48
+ }).strict();
49
+
50
+ export type ApiInfo = z.infer<typeof ApiInfoSchema>;
51
+
52
+ export const HealthyApiStatusSchema = OkaySchema.merge(ApiInfoSchema);
53
+ export type HealthyApiStatus = z.infer<typeof HealthyApiStatusSchema>;
54
+
55
+ export const UnhealthyApiStatusSchema = ApiErrorSchema.merge(ApiInfoSchema);
56
+ export type UnhealthyApiStatus = z.infer<typeof UnhealthyApiStatusSchema>;
57
+
58
+ export const ApiStatusSchema = z.union([
59
+ HealthyApiStatusSchema,
60
+ UnhealthyApiStatusSchema,
61
+ ]);
62
+
63
+ export type ApiStatus = z.infer<typeof ApiStatusSchema>;
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { OkaySchema } from "./okay.schema";
4
+
5
+ describe("OkaySchema", () => {
6
+ it("accepts 2xx status codes", () => {
7
+ expect(OkaySchema.parse({ status: 200 })).toEqual({ status: 200 });
8
+ expect(OkaySchema.parse({ status: 299 })).toEqual({ status: 299 });
9
+ });
10
+
11
+ it("rejects non-2xx status codes", () => {
12
+ expect(() => OkaySchema.parse({ status: 199 })).toThrow();
13
+ expect(() => OkaySchema.parse({ status: 300 })).toThrow();
14
+ });
15
+ });
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+
3
+ export const OkaySchema = z.object({
4
+ status: z.number().min(200).max(299),
5
+ message: z.string().optional(),
6
+ }).strict();
7
+
8
+ export type Okay = z.infer<typeof OkaySchema>;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+
3
+ import { ResultsSchema } from "./results.schema";
4
+
5
+ export const PaginationSchema = z.object({
6
+ page: z.number().describe("Current page number"),
7
+ limit: z.number().describe("Maximum results per page"),
8
+ totalPages: z.number().describe("Total number of pages"),
9
+ }).strict();
10
+
11
+ export type Pagination = z.infer<typeof PaginationSchema>;
12
+
13
+ export const PaginatedResultsSchema = ResultsSchema.extend({
14
+ pagination: PaginationSchema,
15
+ });
16
+
17
+ export type PaginatedResults = z.infer<typeof PaginatedResultsSchema>;
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+
3
+ import { ResultsSchema } from "./results.schema";
4
+
5
+ /**
6
+ * Schema for partial success responses where some items succeeded and others failed.
7
+ * Extends ResultsSchema with an errors count field.
8
+ * Useful for batch operations that can have mixed results (e.g., 422 responses).
9
+ */
10
+ export const PartialResultsSchema = ResultsSchema.extend({
11
+ errors: z.number(), // Count of failed items
12
+ });
13
+ export type PartialResults = z.infer<typeof PartialResultsSchema>;
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { ResultSchema } from "./result.schema";
4
+
5
+ describe("ResultSchema", () => {
6
+ it("transforms timestamp to ISO string", () => {
7
+ const result = ResultSchema.parse({
8
+ status: 200,
9
+ timestamp: "2024-01-02T03:04:05Z"
10
+ });
11
+ expect(result.timestamp).toBe(new Date("2024-01-02T03:04:05Z").toISOString());
12
+ });
13
+
14
+ it("fails when timestamp is invalid", () => {
15
+ expect(() =>
16
+ ResultSchema.parse({ status: 200, timestamp: "invalid" })
17
+ ).toThrow();
18
+ });
19
+ });
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ import { OkaySchema } from "./okay.schema";
4
+
5
+ export const ResultSchema = OkaySchema.extend({
6
+ timestamp: z.string().transform((value) => new Date(value).toISOString()),
7
+ });
8
+
9
+ export type Result = z.infer<typeof ResultSchema>;
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { ResultsSchema } from "./results.schema";
4
+
5
+ describe("ResultsSchema", () => {
6
+ it("parses result count and timestamp", () => {
7
+ const out = ResultsSchema.parse({
8
+ status: 200,
9
+ timestamp: "2024-01-02T03:04:05Z",
10
+ results: 42
11
+ });
12
+ expect(out.results).toBe(42);
13
+ expect(out.timestamp).toBe(new Date("2024-01-02T03:04:05Z").toISOString());
14
+ });
15
+ });
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ import { ResultSchema } from "./result.schema";
4
+
5
+ export const ResultsSchema = ResultSchema.extend({
6
+ results: z.number(),
7
+ });
8
+
9
+ export type Results = z.infer<typeof ResultsSchema>;
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { getCorrelationId } from "./get-correlation-id";
4
+
5
+ describe("getCorrelationId", () => {
6
+ describe("with existing correlation ID", () => {
7
+ it("returns existing correlation ID from Headers object", () => {
8
+ const headers = new Headers();
9
+ headers.set("x-correlation-id", "existing-id-123");
10
+
11
+ const request = { headers };
12
+ const result = getCorrelationId(request);
13
+
14
+ expect(result).toBe("existing-id-123");
15
+ });
16
+
17
+ it("returns existing correlation ID from plain object with get method", () => {
18
+ const headers = {
19
+ get: (name: string) => (name === "x-correlation-id" ? "existing-id-456" : undefined),
20
+ };
21
+
22
+ const request = { headers };
23
+ const result = getCorrelationId(request);
24
+
25
+ expect(result).toBe("existing-id-456");
26
+ });
27
+
28
+ it("is case-insensitive when checking for correlation ID", () => {
29
+ const headers = new Headers();
30
+ headers.set("X-Correlation-ID", "existing-id-uppercase");
31
+
32
+ const request = { headers };
33
+ const result = getCorrelationId(request);
34
+
35
+ expect(result).toBe("existing-id-uppercase");
36
+ });
37
+ });
38
+
39
+ describe("without existing correlation ID", () => {
40
+ it("generates new correlation ID when header is missing", () => {
41
+ const headers = new Headers();
42
+ const request = { headers };
43
+
44
+ const result = getCorrelationId(request);
45
+
46
+ // Should be in format: timestamp-randomhex (timestamp in hex, random part is 16 hex chars)
47
+ expect(result).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
48
+ });
49
+
50
+ it("generates new correlation ID when header is empty string", () => {
51
+ const headers = new Headers();
52
+ headers.set("x-correlation-id", "");
53
+
54
+ const request = { headers };
55
+ const result = getCorrelationId(request);
56
+
57
+ expect(result).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
58
+ });
59
+
60
+ it("generates new correlation ID when headers is undefined", () => {
61
+ const request = {};
62
+ const result = getCorrelationId(request);
63
+
64
+ expect(result).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
65
+ });
66
+
67
+ it("generates new correlation ID when headers.get returns undefined", () => {
68
+ const headers = {
69
+ get: () => {
70
+ // Returns nothing (implicitly undefined)
71
+ },
72
+ };
73
+
74
+ const request = { headers };
75
+ const result = getCorrelationId(request);
76
+
77
+ expect(result).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
78
+ });
79
+
80
+ it("generates unique IDs with different random values", () => {
81
+ const request = { headers: new Headers() };
82
+
83
+ const id1 = getCorrelationId(request);
84
+ const id2 = getCorrelationId(request);
85
+
86
+ expect(id1).not.toBe(id2);
87
+ expect(id1).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
88
+ expect(id2).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
89
+ });
90
+ });
91
+
92
+ describe("edge cases", () => {
93
+ it("handles non-string header values by generating new ID", () => {
94
+ const headers = {
95
+ get: () => 12_345 as unknown as string,
96
+ };
97
+
98
+ const request = { headers };
99
+ const result = getCorrelationId(request);
100
+
101
+ // Non-string values should be treated as truthy and used if they have length
102
+ // But numbers don't have .length property, so they won't pass the check
103
+ // and a new ID will be generated
104
+ expect(result).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
105
+ });
106
+
107
+ it("handles request with no headers property", () => {
108
+ const request = {};
109
+ const result = getCorrelationId(request);
110
+
111
+ // Should generate a new ID with proper format
112
+ expect(result).toMatch(/^[0-9a-f]+-[0-9a-f]{16}$/);
113
+ });
114
+
115
+ it("generates correlation ID with proper format", () => {
116
+ const request = { headers: new Headers() };
117
+ const result = getCorrelationId(request);
118
+
119
+ // Format: timestamp(hex)-16chars(8 bytes in hex)
120
+ const parts = result.split("-");
121
+ expect(parts).toHaveLength(2);
122
+ expect(parts[0]).toMatch(/^[0-9a-f]+$/);
123
+ expect(parts[1]).toMatch(/^[0-9a-f]{16}$/);
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,22 @@
1
+ import { getHeader } from "./get-header";
2
+
3
+ export function getCorrelationId(request: {
4
+ headers?:
5
+ | Headers
6
+ | { get?: (name: string) => string | null; [key: string]: unknown };
7
+ }): string {
8
+ const existingId = getHeader(request, "x-correlation-id");
9
+ if (existingId && typeof existingId === "string" && existingId.length > 0) {
10
+ return existingId;
11
+ }
12
+
13
+ // Use crypto for secure random ID generation
14
+ const bytes = new Uint8Array(8);
15
+ crypto.getRandomValues(bytes);
16
+ let randomPart = "";
17
+ for (const byte of bytes) {
18
+ randomPart += byte.toString(16).padStart(2, "0");
19
+ }
20
+ const timestamp = Date.now().toString(16);
21
+ return `${timestamp}-${randomPart}`;
22
+ }
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { getHeader } from "./get-header";
4
+
5
+ describe("getHeader", () => {
6
+ describe("with Headers object", () => {
7
+ it("returns header value when header exists", () => {
8
+ const headers = new Headers();
9
+ headers.set("content-type", "application/json");
10
+
11
+ const request = { headers };
12
+ const result = getHeader(request, "content-type");
13
+
14
+ expect(result).toBe("application/json");
15
+ });
16
+
17
+ it("returns undefined when header does not exist", () => {
18
+ const headers = new Headers();
19
+
20
+ const request = { headers };
21
+ const result = getHeader(request, "x-missing-header");
22
+
23
+ expect(result).toBeUndefined();
24
+ });
25
+
26
+ it("is case-insensitive for header names", () => {
27
+ const headers = new Headers();
28
+ headers.set("Content-Type", "text/html");
29
+
30
+ const request = { headers };
31
+ const result = getHeader(request, "content-type");
32
+
33
+ expect(result).toBe("text/html");
34
+ });
35
+
36
+ it("handles multiple headers with same name", () => {
37
+ const headers = new Headers();
38
+ headers.append("set-cookie", "cookie1=value1");
39
+ headers.append("set-cookie", "cookie2=value2");
40
+
41
+ const request = { headers };
42
+ const result = getHeader(request, "set-cookie");
43
+
44
+ // Headers.get() returns comma-separated values for duplicate headers
45
+ expect(result).toContain("cookie1=value1");
46
+ });
47
+ });
48
+
49
+ describe("with plain object (with get method)", () => {
50
+ it("returns header value when header exists", () => {
51
+ const headers = {
52
+ get: (name: string) => {
53
+ const headerMap: Record<string, string> = {
54
+ "content-type": "application/xml",
55
+ authorization: "Bearer token123",
56
+ };
57
+ return headerMap[name] || undefined;
58
+ },
59
+ };
60
+
61
+ const request = { headers };
62
+ const result = getHeader(request, "content-type");
63
+
64
+ expect(result).toBe("application/xml");
65
+ });
66
+
67
+ it("returns undefined when get method returns undefined", () => {
68
+ const headers = {
69
+ get: () => {
70
+ // Returns nothing (implicitly undefined)
71
+ },
72
+ };
73
+
74
+ const request = { headers };
75
+ const result = getHeader(request, "x-custom-header");
76
+
77
+ expect(result).toBeUndefined();
78
+ });
79
+ });
80
+
81
+ describe("with plain object (without get method)", () => {
82
+ it("returns header value when header exists (case-insensitive)", () => {
83
+ const headers = {
84
+ "Content-Type": "application/json",
85
+ Authorization: "Bearer token456",
86
+ };
87
+
88
+ const request = { headers };
89
+ const result = getHeader(request, "content-type");
90
+
91
+ expect(result).toBe("application/json");
92
+ });
93
+
94
+ it("returns header value with different case", () => {
95
+ const headers = {
96
+ "x-custom-header": "custom-value",
97
+ };
98
+
99
+ const request = { headers };
100
+ const result = getHeader(request, "X-Custom-Header");
101
+
102
+ expect(result).toBe("custom-value");
103
+ });
104
+
105
+ it("returns undefined when header does not exist", () => {
106
+ const headers = {
107
+ "content-type": "text/plain",
108
+ };
109
+
110
+ const request = { headers };
111
+ const result = getHeader(request, "x-nonexistent");
112
+
113
+ expect(result).toBeUndefined();
114
+ });
115
+
116
+ it("converts non-string header values to strings", () => {
117
+ const headers = {
118
+ "x-numeric-header": 12_345,
119
+ "x-boolean-header": true,
120
+ };
121
+
122
+ const request = { headers };
123
+
124
+ expect(getHeader(request, "x-numeric-header")).toBe("12345");
125
+ expect(getHeader(request, "x-boolean-header")).toBe("true");
126
+ });
127
+ });
128
+
129
+ describe("edge cases", () => {
130
+ it("returns undefined when headers is undefined", () => {
131
+ const request = {};
132
+ const result = getHeader(request, "any-header");
133
+
134
+ expect(result).toBeUndefined();
135
+ });
136
+
137
+ it("returns undefined when headers is undefined in request", () => {
138
+ const request = { headers: undefined };
139
+ const result = getHeader(request as { headers: Headers }, "any-header");
140
+
141
+ expect(result).toBeUndefined();
142
+ });
143
+
144
+ it("returns undefined when request has no headers property", () => {
145
+ const request = {};
146
+ const result = getHeader(request, "content-type");
147
+
148
+ expect(result).toBeUndefined();
149
+ });
150
+
151
+ it("handles empty header name edge case", () => {
152
+ const headers = {
153
+ "": "empty-name-value",
154
+ "content-type": "application/json",
155
+ };
156
+
157
+ const request = { headers };
158
+
159
+ // Empty string key can be found but may not be accessible in all environments
160
+ // Test that it doesn't throw and returns a value (either the value or undefined)
161
+ const result = getHeader(request, "");
162
+ expect(result === "empty-name-value" || result === undefined).toBe(true);
163
+
164
+ // Normal keys should still work
165
+ expect(getHeader(request, "content-type")).toBe("application/json");
166
+ });
167
+
168
+ it("handles headers with special characters in names", () => {
169
+ const headers = {
170
+ "x-special-!@#$-header": "special-value",
171
+ };
172
+
173
+ const request = { headers };
174
+ const result = getHeader(request, "X-Special-!@#$-Header");
175
+
176
+ expect(result).toBe("special-value");
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,21 @@
1
+ export function getHeader(
2
+ request: {
3
+ headers?:
4
+ | Headers
5
+ | { get?: (name: string) => string | null; [key: string]: unknown };
6
+ },
7
+ name: string
8
+ ): string | undefined {
9
+ const headers = request?.headers;
10
+ if (!headers) return undefined;
11
+
12
+ if (typeof headers.get === "function") {
13
+ const v = headers.get(name);
14
+ return v === null ? undefined : v;
15
+ }
16
+
17
+ const key = Object.keys(headers).find(
18
+ (k) => k.toLowerCase() === name.toLowerCase()
19
+ );
20
+ return key ? String((headers as Record<string, unknown>)[key]) : undefined;
21
+ }