@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
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ // Schemas
2
+ export * from "./api-schemas/error.schema";
3
+ export * from "./api-schemas/health.schema";
4
+ export * from "./api-schemas/okay.schema";
5
+ export * from "./api-schemas/result.schema";
6
+ export * from "./api-schemas/results.schema";
7
+
8
+ // Primitive schemas
9
+ export * from "./primitives/location.schema";
10
+ export * from "./primitives/slug.schema";
11
+ export * from "./primitives/numeric-id.schema";
12
+ export * from "./primitives/uuid.schema";
13
+ export * from "./primitives/geoname-id.schema";
14
+ export * from "./primitives/wikidata-id.schema";
15
+ export * from "./primitives/international-zip.schema";
16
+
17
+ // Time utilities
18
+ export * from "./time/timezone.types";
19
+ export * from "./time/iso8601.schema"; // ISO8601 schemas
20
+ export * from "./time/relative-time.schema"; // Relative time schemas
21
+ export * from "./time/flexible-time.schema"; // Flexible time schemas
22
+ export * from "./time/bounded-time.schema"; // Bounded time schemas
23
+ export * from "./time/time-helpers"; // Helper functions
24
+ export * from "./time/is-relative-time";
25
+ export { parseFlexibleTime as parseFlexibleTimeAdvanced } from "./time/flexible-time-parser";
26
+ export * from "./time/boundary-enforcement";
27
+
28
+ // Deprecated exports (for backward compatibility)
29
+ /** @deprecated Use flexible-time.schema exports instead */
30
+ export { FlexibleTimeSchema as FlexibleTimeSchemaOld, FlexibleTime as FlexibleTimeOld, TimeUnit, RelativeTimeConfig } from "./time/time-schemas";
31
+
32
+ // Helpers
33
+ export * from "./helpers/correlation/get-correlation-id";
34
+ export * from "./helpers/correlation/get-header";
35
+ export * from "./helpers/detect-mime-type";
36
+ export * from "./helpers/detect-suspicious-patterns";
37
+ export * from "./helpers/hash-binary";
38
+ export * from "./helpers/parameter-validation";
39
+ export * from "./helpers/slugify";
40
+ export * from "./helpers/response-headers/build-api-unauthorized-headers";
41
+ export * from "./helpers/response-headers/environment.types";
42
+ export * from "./helpers/response-headers/resolve-environment";
43
+
44
+ // Utilities
45
+ export * from "./normalization/normalize-list";
46
+ export * from "./normalization/normalize-location";
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { normalizeStringList } from "./normalize-list";
4
+
5
+ describe("normalizeStringList", () => {
6
+ it("lowercases, removes duplicates, and sorts values with proper umlaut handling", () => {
7
+ const input = [
8
+ "Banana",
9
+ "apple",
10
+ "cherry",
11
+ "Äpfel",
12
+ " banana ",
13
+ "Öl",
14
+ "orange",
15
+ ];
16
+ const result = normalizeStringList(input);
17
+ expect(result).toEqual([
18
+ "äpfel",
19
+ "apple",
20
+ "banana",
21
+ "cherry",
22
+ "öl",
23
+ "orange",
24
+ ]);
25
+ });
26
+
27
+ it("handles empty and already normalized inputs", () => {
28
+ expect(normalizeStringList([])).toEqual([]);
29
+ expect(normalizeStringList(["a", "b"])).toEqual(["a", "b"]);
30
+ });
31
+
32
+ it("removes duplicates correctly", () => {
33
+ const input = ["apple", "Apple", "APPLE", " apple ", "banana"];
34
+ const result = normalizeStringList(input);
35
+ expect(result).toEqual(["apple", "banana"]);
36
+ });
37
+
38
+ it("sorts umlauts near their base letters", () => {
39
+ const input = ["z", "ä", "a", "ö", "o", "ü", "u", "b"];
40
+ const result = normalizeStringList(input);
41
+ expect(result).toEqual(["a", "ä", "b", "o", "ö", "u", "ü", "z"]);
42
+ });
43
+ });
@@ -0,0 +1,21 @@
1
+ import { MAX_TAGS_COUNT, sanitizeTag } from "@schafevormfenster/security";
2
+
3
+ /**
4
+ * Normalize an array of strings by:
5
+ * - sanitizing with whitelist rules (preserving umlauts, removing injections)
6
+ * - trimming whitespace
7
+ * - lowercasing using locale-insensitive toLowerCase
8
+ * - removing duplicates
9
+ * - sorting alphabetically with proper German locale support (umlauts near base letters)
10
+ * - enforcing a maximum count
11
+ */
12
+ export function normalizeStringList(values: string[] = []): string[] {
13
+ const sanitized = values.map((v) => sanitizeTag(v));
14
+ const normalized = sanitized
15
+ .map((v) => v.trim().toLowerCase())
16
+ .filter((v) => v.length > 0);
17
+
18
+ const unique = [...new Set(normalized)].slice(0, MAX_TAGS_COUNT);
19
+ unique.sort((a, b) => a.localeCompare(b, "de-DE"));
20
+ return unique;
21
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { normalizeLocation } from './normalize-location';
4
+
5
+ describe('normalizeLocation', () => {
6
+ describe('space normalization', () => {
7
+ it('should collapse multiple spaces to single space', () => {
8
+ expect(normalizeLocation('Schlatkow 17390 Schmatzin')).toBe('Schlatkow 17390 Schmatzin');
9
+ expect(normalizeLocation('Berlin Germany')).toBe('Berlin Germany');
10
+ expect(normalizeLocation('Am Markt 5')).toBe('Am Markt 5');
11
+ });
12
+
13
+ it('should handle tabs and newlines as spaces', () => {
14
+ expect(normalizeLocation('Schlatkow\t17390\nSchmatzin')).toBe('Schlatkow 17390 Schmatzin');
15
+ expect(normalizeLocation('Berlin\t\tGermany\n')).toBe('Berlin Germany');
16
+ });
17
+
18
+ it('should trim leading and trailing spaces', () => {
19
+ expect(normalizeLocation(' Schlatkow 17390 Schmatzin ')).toBe('Schlatkow 17390 Schmatzin');
20
+ expect(normalizeLocation('\tBerlin Germany\n')).toBe('Berlin Germany');
21
+ });
22
+ });
23
+
24
+ describe('comma normalization', () => {
25
+ it('should ensure no space before comma and one space after', () => {
26
+ expect(normalizeLocation('Schlatkow ,17390 Schmatzin')).toBe('Schlatkow, 17390 Schmatzin');
27
+ expect(normalizeLocation('Schlatkow , 17390 Schmatzin')).toBe('Schlatkow, 17390 Schmatzin');
28
+ expect(normalizeLocation('Schlatkow,17390 Schmatzin')).toBe('Schlatkow, 17390 Schmatzin');
29
+ });
30
+
31
+ it('should handle multiple commas correctly', () => {
32
+ expect(normalizeLocation('Schlatkow , 17390 , Schmatzin')).toBe('Schlatkow, 17390, Schmatzin');
33
+ expect(normalizeLocation('A,B,C,D')).toBe('A, B, C, D');
34
+ });
35
+
36
+ it('should handle comma at the end correctly', () => {
37
+ expect(normalizeLocation('Schlatkow, 17390 Schmatzin ,')).toBe('Schlatkow, 17390 Schmatzin,');
38
+ expect(normalizeLocation('Berlin , ')).toBe('Berlin,');
39
+ });
40
+ });
41
+
42
+ describe('idempotent behavior', () => {
43
+ it('should handle already normalized strings (idempotent)', () => {
44
+ const normalized = 'Schlatkow, 17390 Schmatzin';
45
+ expect(normalizeLocation(normalized)).toBe(normalized);
46
+ });
47
+ });
48
+
49
+ describe('edge cases', () => {
50
+ it('should handle empty string', () => {
51
+ expect(normalizeLocation('')).toBe('');
52
+ });
53
+
54
+ it('should handle string with only spaces', () => {
55
+ expect(normalizeLocation(' ')).toBe('');
56
+ expect(normalizeLocation('\t\n')).toBe('');
57
+ });
58
+
59
+ it('should handle string with only comma', () => {
60
+ expect(normalizeLocation(',')).toBe(',');
61
+ expect(normalizeLocation(' , ')).toBe(',');
62
+ });
63
+
64
+ it('should handle single character', () => {
65
+ expect(normalizeLocation('A')).toBe('A');
66
+ expect(normalizeLocation(' A ')).toBe('A');
67
+ });
68
+
69
+ it('should handle special characters and numbers', () => {
70
+ expect(normalizeLocation('Str. 123 , 12345 City')).toBe('Str. 123, 12345 City');
71
+ expect(normalizeLocation('Ä-Ö-Ü , Test')).toBe('Ä-Ö-Ü, Test');
72
+ });
73
+ });
74
+
75
+ describe('cache optimization scenarios', () => {
76
+ it('should normalize different variations to the same string', () => {
77
+ const variations = [
78
+ 'Schlatkow, 17390 Schmatzin',
79
+ 'Schlatkow , 17390 Schmatzin',
80
+ 'Schlatkow ,17390 Schmatzin',
81
+ ' Schlatkow , 17390 Schmatzin ',
82
+ 'Schlatkow\t,\t17390\tSchmatzin',
83
+ ];
84
+
85
+ const expected = 'Schlatkow, 17390 Schmatzin';
86
+ for (const variation of variations) {
87
+ expect(normalizeLocation(variation)).toBe(expected);
88
+ }
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Normalizes location strings to optimize cache hits.
3
+ *
4
+ * Cleanup operations:
5
+ * - Remove duplicate or multiple spaces and reduce to at most one space character
6
+ * - Ensure that a comma has no space before but one space after
7
+ *
8
+ * @param location - The location string to normalize
9
+ * @returns Normalized location string
10
+ *
11
+ * @example
12
+ * normalizeLocation("Schlatkow , 17390 Schmatzin")
13
+ * // Returns: "Schlatkow, 17390 Schmatzin"
14
+ */
15
+ export function normalizeLocation(location: string): string {
16
+ return (
17
+ location
18
+ // First pass: normalize spaces (collapse multiple spaces/tabs/newlines to single space)
19
+
20
+ .replaceAll(/[ \t\n\r]+/gu, " ")
21
+ // Trim leading and trailing spaces
22
+ .trim()
23
+ // Fix comma spacing: remove space before comma, ensure one space after comma (but not at end)
24
+ // eslint-disable-next-line sonarjs/slow-regex -- Necessary for comma normalization
25
+ .replaceAll(/ *, */gu, ", ")
26
+ // Remove trailing space after comma if it's at the end of string
27
+ .replace(/, $/u, ",")
28
+ );
29
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { hasValidDecimalPrecision } from "./coordinate-precision";
4
+
5
+ describe("hasValidDecimalPrecision", () => {
6
+ it("should accept values with no decimal places", () => {
7
+ expect(hasValidDecimalPrecision(0)).toBe(true);
8
+ expect(hasValidDecimalPrecision(90)).toBe(true);
9
+ expect(hasValidDecimalPrecision(-180)).toBe(true);
10
+ });
11
+
12
+ it("should accept values with up to 8 decimal places", () => {
13
+ expect(hasValidDecimalPrecision(52.52)).toBe(true); // 2 decimal places
14
+ expect(hasValidDecimalPrecision(40.7128)).toBe(true); // 4 decimal places
15
+ expect(hasValidDecimalPrecision(52.520_008)).toBe(true); // 6 decimal places
16
+ expect(hasValidDecimalPrecision(52.520_008_07)).toBe(true); // 8 decimal places
17
+ });
18
+
19
+ it("should reject values with more than 8 decimal places", () => {
20
+ expect(hasValidDecimalPrecision(52.520_008_071)).toBe(false); // 9 decimal places
21
+ expect(hasValidDecimalPrecision(40.712_812_345_678)).toBe(false); // 12 decimal places
22
+ });
23
+
24
+ it("should handle scientific notation correctly", () => {
25
+ // Small numbers with few actual decimal places should pass
26
+ expect(hasValidDecimalPrecision(1.23e-4)).toBe(true); // 0.000123 = 6 decimals
27
+ expect(hasValidDecimalPrecision(1.23e-2)).toBe(true); // 0.0123 = 4 decimals
28
+
29
+ // Numbers with more than 8 decimal places should fail
30
+ expect(hasValidDecimalPrecision(1.234_567_89e-1)).toBe(false); // 0.123456789 = 9 decimals
31
+ });
32
+
33
+ it("should support custom max decimal places", () => {
34
+ expect(hasValidDecimalPrecision(1.234, 3)).toBe(true);
35
+ expect(hasValidDecimalPrecision(1.2345, 3)).toBe(false);
36
+ expect(hasValidDecimalPrecision(1.234_56, 5)).toBe(true);
37
+ expect(hasValidDecimalPrecision(1.234_567, 5)).toBe(false);
38
+ });
39
+
40
+ it("should handle edge cases", () => {
41
+ expect(hasValidDecimalPrecision(0)).toBe(true);
42
+ expect(hasValidDecimalPrecision(-0)).toBe(true);
43
+ // Number.EPSILON has many decimal places when converted to standard notation
44
+ expect(hasValidDecimalPrecision(Number.EPSILON)).toBe(false);
45
+ });
46
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Validates that a coordinate value has at most the specified number of decimal places.
3
+ * Handles both standard notation and scientific notation.
4
+ *
5
+ * @param value - The numeric coordinate value to validate
6
+ * @param maxDecimalPlaces - Maximum allowed decimal places (default: 8)
7
+ * @returns true if the value has at most maxDecimalPlaces decimal places
8
+ */
9
+ export function hasValidDecimalPrecision(
10
+ value: number,
11
+ maxDecimalPlaces = 8
12
+ ): boolean {
13
+ // Convert to string and handle scientific notation
14
+ const valueString = value.toString();
15
+ if (valueString.includes("e") || valueString.includes("E")) {
16
+ // For scientific notation, convert to fixed notation
17
+ // Use 20 decimal places to ensure we capture all significant digits
18
+ // without losing precision during the conversion
19
+ const fixed = value.toFixed(20);
20
+ const [, decimalPart = ""] = fixed.split(".");
21
+ // Remove trailing zeros without regex
22
+ let decimals = decimalPart;
23
+ while (decimals.endsWith("0")) {
24
+ decimals = decimals.slice(0, -1);
25
+ }
26
+ return decimals.length <= maxDecimalPlaces;
27
+ }
28
+ const decimalPlaces = valueString.split(".")[1]?.length || 0;
29
+ return decimalPlaces <= maxDecimalPlaces;
30
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { GeoPointSchema } from "./geo-point.schema";
4
+
5
+ describe("GeoPointSchema", () => {
6
+ it("should accept valid geo points", () => {
7
+ expect(GeoPointSchema.parse([0, 0])).toEqual([0, 0]);
8
+ expect(GeoPointSchema.parse([52.52, 13.405])).toEqual([52.52, 13.405]); // Berlin
9
+ expect(GeoPointSchema.parse([40.7128, -74.006])).toEqual([40.7128, -74.006]); // New York
10
+ expect(GeoPointSchema.parse([-33.8688, 151.2093])).toEqual([-33.8688, 151.2093]); // Sydney
11
+ });
12
+
13
+ it("should accept boundary values", () => {
14
+ expect(GeoPointSchema.parse([90, 180])).toEqual([90, 180]); // North Pole, Date Line
15
+ expect(GeoPointSchema.parse([-90, -180])).toEqual([-90, -180]); // South Pole, Date Line
16
+ expect(GeoPointSchema.parse([90, -180])).toEqual([90, -180]);
17
+ expect(GeoPointSchema.parse([-90, 180])).toEqual([-90, 180]);
18
+ });
19
+
20
+ it("should reject invalid latitude", () => {
21
+ expect(() => GeoPointSchema.parse([91, 0])).toThrow(
22
+ "Latitude must be less than or equal to 90"
23
+ );
24
+ expect(() => GeoPointSchema.parse([-91, 0])).toThrow(
25
+ "Latitude must be greater than or equal to -90"
26
+ );
27
+ expect(() => GeoPointSchema.parse([100, 0])).toThrow(
28
+ "Latitude must be less than or equal to 90"
29
+ );
30
+ });
31
+
32
+ it("should reject invalid longitude", () => {
33
+ expect(() => GeoPointSchema.parse([0, 181])).toThrow(
34
+ "Longitude must be less than or equal to 180"
35
+ );
36
+ expect(() => GeoPointSchema.parse([0, -181])).toThrow(
37
+ "Longitude must be greater than or equal to -180"
38
+ );
39
+ expect(() => GeoPointSchema.parse([0, 360])).toThrow(
40
+ "Longitude must be less than or equal to 180"
41
+ );
42
+ });
43
+
44
+ it("should reject tuples with wrong length", () => {
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ expect(() => GeoPointSchema.parse([0] as any)).toThrow();
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ expect(() => GeoPointSchema.parse([0, 0, 0] as any)).toThrow();
49
+ });
50
+
51
+ it("should reject non-tuple values", () => {
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ expect(() => GeoPointSchema.parse("0,0" as any)).toThrow();
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ expect(() => GeoPointSchema.parse({ lat: 0, lng: 0 } as any)).toThrow();
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/no-null
57
+ expect(() => GeoPointSchema.parse(null as any)).toThrow();
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ expect(() => GeoPointSchema.parse(undefined as any)).toThrow();
60
+ });
61
+
62
+ it("should reject non-numeric coordinates", () => {
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ expect(() => GeoPointSchema.parse(["0", "0"] as any)).toThrow();
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ expect(() => GeoPointSchema.parse([0, "0"] as any)).toThrow();
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ expect(() => GeoPointSchema.parse(["0", 0] as any)).toThrow();
69
+ });
70
+ });
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+
3
+ import { LatitudeSchema } from "./latitude.schema";
4
+ import { LongitudeSchema } from "./longitude.schema";
5
+
6
+ /**
7
+ * Zod schema for validating geographic coordinates as a tuple.
8
+ * Geographic coordinates as [latitude, longitude].
9
+ */
10
+ export const GeoPointSchema = z
11
+ .tuple([LatitudeSchema, LongitudeSchema])
12
+ .describe("Geographic coordinates as [latitude, longitude]");
13
+
14
+ export type GeoPoint = z.infer<typeof GeoPointSchema>;
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { GeonameIdSchema } from "./geoname-id.schema";
4
+
5
+ describe("GeonameIdSchema", () => {
6
+ it("should accept valid Geoname IDs", () => {
7
+ expect(GeonameIdSchema.parse("1")).toBe("1");
8
+ expect(GeonameIdSchema.parse("2950159")).toBe("2950159"); // Berlin
9
+ expect(GeonameIdSchema.parse("5128581")).toBe("5128581"); // New York
10
+ expect(GeonameIdSchema.parse("1234567890")).toBe("1234567890"); // 10 digits max
11
+ });
12
+
13
+ it("should reject IDs starting with zero", () => {
14
+ expect(() => GeonameIdSchema.parse("0")).toThrow(
15
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
16
+ );
17
+ expect(() => GeonameIdSchema.parse("01234")).toThrow(
18
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
19
+ );
20
+ });
21
+
22
+ it("should reject IDs longer than 10 digits", () => {
23
+ expect(() => GeonameIdSchema.parse("12345678901")).toThrow(
24
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
25
+ );
26
+ });
27
+
28
+ it("should reject non-numeric strings", () => {
29
+ expect(() => GeonameIdSchema.parse("abc")).toThrow(
30
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
31
+ );
32
+ expect(() => GeonameIdSchema.parse("123abc")).toThrow(
33
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
34
+ );
35
+ expect(() => GeonameIdSchema.parse("12-34")).toThrow(
36
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
37
+ );
38
+ });
39
+
40
+ it("should reject empty strings", () => {
41
+ expect(() => GeonameIdSchema.parse("")).toThrow(
42
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
43
+ );
44
+ });
45
+
46
+ it("should reject negative numbers", () => {
47
+ expect(() => GeonameIdSchema.parse("-123")).toThrow(
48
+ "Must be a valid Geoname ID (positive integer, max 10 digits)"
49
+ );
50
+ });
51
+
52
+ it("should reject non-string values", () => {
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ expect(() => GeonameIdSchema.parse(123 as any)).toThrow();
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/no-null
56
+ expect(() => GeonameIdSchema.parse(null as any)).toThrow();
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ expect(() => GeonameIdSchema.parse(undefined as any)).toThrow();
59
+ });
60
+ });
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Zod schema for validating Geonames.org numeric IDs.
5
+ * Geoname IDs are positive integers, typically 1-10 digits.
6
+ */
7
+ export const GeonameIdSchema = z
8
+ .string()
9
+ .regex(/^[1-9]\d{0,9}$/, "Must be a valid Geoname ID (positive integer, max 10 digits)")
10
+ .describe("Geonames.org location identifier");
11
+
12
+ export type GeonameId = z.infer<typeof GeonameIdSchema>;