@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.
- package/CONTRIBUTING.md +1190 -0
- package/README.md +275 -0
- package/bin/setup.js +10 -0
- package/dist/api-schemas/error.schema.d.ts +20 -0
- package/dist/api-schemas/error.schema.d.ts.map +1 -0
- package/dist/api-schemas/error.schema.js +17 -0
- package/dist/api-schemas/health.schema.d.ts +497 -0
- package/dist/api-schemas/health.schema.d.ts.map +1 -0
- package/dist/api-schemas/health.schema.js +33 -0
- package/dist/api-schemas/okay.schema.d.ts +13 -0
- package/dist/api-schemas/okay.schema.d.ts.map +1 -0
- package/dist/api-schemas/okay.schema.js +5 -0
- package/dist/api-schemas/paginated-results.schema.d.ts +59 -0
- package/dist/api-schemas/paginated-results.schema.d.ts.map +1 -0
- package/dist/api-schemas/paginated-results.schema.js +10 -0
- package/dist/api-schemas/partial-results.schema.d.ts +30 -0
- package/dist/api-schemas/partial-results.schema.d.ts.map +1 -0
- package/dist/api-schemas/partial-results.schema.js +10 -0
- package/dist/api-schemas/result.schema.d.ts +17 -0
- package/dist/api-schemas/result.schema.d.ts.map +1 -0
- package/dist/api-schemas/result.schema.js +5 -0
- package/dist/api-schemas/results.schema.d.ts +21 -0
- package/dist/api-schemas/results.schema.d.ts.map +1 -0
- package/dist/api-schemas/results.schema.js +5 -0
- package/dist/helpers/correlation/get-correlation-id.d.ts +7 -0
- package/dist/helpers/correlation/get-correlation-id.d.ts.map +1 -0
- package/dist/helpers/correlation/get-correlation-id.js +16 -0
- package/dist/helpers/correlation/get-header.d.ts +7 -0
- package/dist/helpers/correlation/get-header.d.ts.map +1 -0
- package/dist/helpers/correlation/get-header.js +11 -0
- package/dist/helpers/detect-mime-type.d.ts +11 -0
- package/dist/helpers/detect-mime-type.d.ts.map +1 -0
- package/dist/helpers/detect-mime-type.js +40 -0
- package/dist/helpers/detect-suspicious-patterns.d.ts +8 -0
- package/dist/helpers/detect-suspicious-patterns.d.ts.map +1 -0
- package/dist/helpers/detect-suspicious-patterns.js +55 -0
- package/dist/helpers/eventify-constants.types.d.ts +32 -0
- package/dist/helpers/eventify-constants.types.d.ts.map +1 -0
- package/dist/helpers/eventify-constants.types.js +40 -0
- package/dist/helpers/hash-binary.d.ts +21 -0
- package/dist/helpers/hash-binary.d.ts.map +1 -0
- package/dist/helpers/hash-binary.js +28 -0
- package/dist/helpers/mime-types/detect-image-mime-type.d.ts +5 -0
- package/dist/helpers/mime-types/detect-image-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-image-mime-type.js +41 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.d.ts +6 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.js +34 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts +5 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.js +13 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.d.ts +6 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.js +23 -0
- package/dist/helpers/parameter-validation.d.ts +6 -0
- package/dist/helpers/parameter-validation.d.ts.map +1 -0
- package/dist/helpers/parameter-validation.js +19 -0
- package/dist/helpers/parameter-validation.types.d.ts +16 -0
- package/dist/helpers/parameter-validation.types.d.ts.map +1 -0
- package/dist/helpers/parameter-validation.types.js +38 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts +6 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts.map +1 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.js +23 -0
- package/dist/helpers/response-headers/environment.types.d.ts +2 -0
- package/dist/helpers/response-headers/environment.types.d.ts.map +1 -0
- package/dist/helpers/response-headers/environment.types.js +1 -0
- package/dist/helpers/response-headers/resolve-environment.d.ts +8 -0
- package/dist/helpers/response-headers/resolve-environment.d.ts.map +1 -0
- package/dist/helpers/response-headers/resolve-environment.js +18 -0
- package/dist/helpers/slugify.d.ts +15 -0
- package/dist/helpers/slugify.d.ts.map +1 -0
- package/dist/helpers/slugify.js +32 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/normalization/normalize-list.d.ts +11 -0
- package/dist/normalization/normalize-list.d.ts.map +1 -0
- package/dist/normalization/normalize-list.js +19 -0
- package/dist/normalization/normalize-location.d.ts +16 -0
- package/dist/normalization/normalize-location.d.ts.map +1 -0
- package/dist/normalization/normalize-location.js +26 -0
- package/dist/primitives/coordinate-precision.d.ts +10 -0
- package/dist/primitives/coordinate-precision.d.ts.map +1 -0
- package/dist/primitives/coordinate-precision.js +27 -0
- package/dist/primitives/geo-point.schema.d.ts +8 -0
- package/dist/primitives/geo-point.schema.d.ts.map +1 -0
- package/dist/primitives/geo-point.schema.js +10 -0
- package/dist/primitives/geoname-id.schema.d.ts +8 -0
- package/dist/primitives/geoname-id.schema.d.ts.map +1 -0
- package/dist/primitives/geoname-id.schema.js +9 -0
- package/dist/primitives/international-zip.schema.d.ts +76 -0
- package/dist/primitives/international-zip.schema.d.ts.map +1 -0
- package/dist/primitives/international-zip.schema.js +81 -0
- package/dist/primitives/latitude.schema.d.ts +9 -0
- package/dist/primitives/latitude.schema.d.ts.map +1 -0
- package/dist/primitives/latitude.schema.js +13 -0
- package/dist/primitives/location.schema.d.ts +8 -0
- package/dist/primitives/location.schema.d.ts.map +1 -0
- package/dist/primitives/location.schema.js +15 -0
- package/dist/primitives/longitude.schema.d.ts +9 -0
- package/dist/primitives/longitude.schema.d.ts.map +1 -0
- package/dist/primitives/longitude.schema.js +13 -0
- package/dist/primitives/numeric-id.schema.d.ts +8 -0
- package/dist/primitives/numeric-id.schema.d.ts.map +1 -0
- package/dist/primitives/numeric-id.schema.js +10 -0
- package/dist/primitives/slug.schema.d.ts +17 -0
- package/dist/primitives/slug.schema.d.ts.map +1 -0
- package/dist/primitives/slug.schema.js +30 -0
- package/dist/primitives/uuid.schema.d.ts +8 -0
- package/dist/primitives/uuid.schema.d.ts.map +1 -0
- package/dist/primitives/uuid.schema.js +9 -0
- package/dist/primitives/wikidata-id.schema.d.ts +9 -0
- package/dist/primitives/wikidata-id.schema.d.ts.map +1 -0
- package/dist/primitives/wikidata-id.schema.js +10 -0
- package/dist/time/boundary-enforcement.d.ts +11 -0
- package/dist/time/boundary-enforcement.d.ts.map +1 -0
- package/dist/time/boundary-enforcement.js +43 -0
- package/dist/time/bounded-time.schema.d.ts +31 -0
- package/dist/time/bounded-time.schema.d.ts.map +1 -0
- package/dist/time/bounded-time.schema.js +77 -0
- package/dist/time/flexible-time-parser.d.ts +12 -0
- package/dist/time/flexible-time-parser.d.ts.map +1 -0
- package/dist/time/flexible-time-parser.js +94 -0
- package/dist/time/flexible-time.schema.d.ts +31 -0
- package/dist/time/flexible-time.schema.d.ts.map +1 -0
- package/dist/time/flexible-time.schema.js +31 -0
- package/dist/time/get-week-end.d.ts +10 -0
- package/dist/time/get-week-end.d.ts.map +1 -0
- package/dist/time/get-week-end.js +25 -0
- package/dist/time/get-week-start.d.ts +10 -0
- package/dist/time/get-week-start.d.ts.map +1 -0
- package/dist/time/get-week-start.js +25 -0
- package/dist/time/is-relative-time.d.ts +8 -0
- package/dist/time/is-relative-time.d.ts.map +1 -0
- package/dist/time/is-relative-time.js +9 -0
- package/dist/time/iso8601.schema.d.ts +14 -0
- package/dist/time/iso8601.schema.d.ts.map +1 -0
- package/dist/time/iso8601.schema.js +17 -0
- package/dist/time/iso8601.types.d.ts +6 -0
- package/dist/time/iso8601.types.d.ts.map +1 -0
- package/dist/time/iso8601.types.js +11 -0
- package/dist/time/parse-relative-time.d.ts +9 -0
- package/dist/time/parse-relative-time.d.ts.map +1 -0
- package/dist/time/parse-relative-time.js +36 -0
- package/dist/time/relative-time.schema.d.ts +23 -0
- package/dist/time/relative-time.schema.d.ts.map +1 -0
- package/dist/time/relative-time.schema.js +25 -0
- package/dist/time/since-parameter.schema.d.ts +8 -0
- package/dist/time/since-parameter.schema.d.ts.map +1 -0
- package/dist/time/since-parameter.schema.js +56 -0
- package/dist/time/time-helpers.d.ts +19 -0
- package/dist/time/time-helpers.d.ts.map +1 -0
- package/dist/time/time-helpers.js +56 -0
- package/dist/time/time-schemas.d.ts +20 -0
- package/dist/time/time-schemas.d.ts.map +1 -0
- package/dist/time/time-schemas.js +25 -0
- package/dist/time/timezone.types.d.ts +17 -0
- package/dist/time/timezone.types.d.ts.map +1 -0
- package/dist/time/timezone.types.js +15 -0
- package/dist/validation/zod-error-handler.d.ts +3 -0
- package/dist/validation/zod-error-handler.d.ts.map +1 -0
- package/dist/validation/zod-error-handler.js +189 -0
- package/dist/validation/zod-utils.d.ts +9 -0
- package/dist/validation/zod-utils.d.ts.map +1 -0
- package/dist/validation/zod-utils.js +23 -0
- package/eslint.config.mjs +16 -0
- package/package.json +44 -0
- package/src/api-schemas/error.schema.test.ts +27 -0
- package/src/api-schemas/error.schema.ts +23 -0
- package/src/api-schemas/health.schema.test.ts +104 -0
- package/src/api-schemas/health.schema.ts +63 -0
- package/src/api-schemas/okay.schema.test.ts +15 -0
- package/src/api-schemas/okay.schema.ts +8 -0
- package/src/api-schemas/paginated-results.schema.ts +17 -0
- package/src/api-schemas/partial-results.schema.ts +13 -0
- package/src/api-schemas/result.schema.test.ts +19 -0
- package/src/api-schemas/result.schema.ts +9 -0
- package/src/api-schemas/results.schema.test.ts +15 -0
- package/src/api-schemas/results.schema.ts +9 -0
- package/src/helpers/correlation/get-correlation-id.test.ts +126 -0
- package/src/helpers/correlation/get-correlation-id.ts +22 -0
- package/src/helpers/correlation/get-header.test.ts +179 -0
- package/src/helpers/correlation/get-header.ts +21 -0
- package/src/helpers/detect-mime-type.test.ts +100 -0
- package/src/helpers/detect-mime-type.ts +46 -0
- package/src/helpers/detect-suspicious-patterns.test.ts +45 -0
- package/src/helpers/detect-suspicious-patterns.ts +57 -0
- package/src/helpers/eventify-constants.test.ts +52 -0
- package/src/helpers/eventify-constants.types.test.ts +52 -0
- package/src/helpers/eventify-constants.types.ts +51 -0
- package/src/helpers/hash-binary.test.ts +60 -0
- package/src/helpers/hash-binary.ts +30 -0
- package/src/helpers/mime-types/detect-image-mime-type.test.ts +73 -0
- package/src/helpers/mime-types/detect-image-mime-type.ts +50 -0
- package/src/helpers/mime-types/detect-ole-mime-type.test.ts +86 -0
- package/src/helpers/mime-types/detect-ole-mime-type.ts +44 -0
- package/src/helpers/mime-types/detect-pdf-mime-type.test.ts +39 -0
- package/src/helpers/mime-types/detect-pdf-mime-type.ts +15 -0
- package/src/helpers/mime-types/detect-zip-mime-type.test.ts +88 -0
- package/src/helpers/mime-types/detect-zip-mime-type.ts +28 -0
- package/src/helpers/parameter-validation.test.ts +35 -0
- package/src/helpers/parameter-validation.ts +32 -0
- package/src/helpers/process-eventify-request.ts +146 -0
- package/src/helpers/response-headers/build-api-unauthorized-headers.ts +30 -0
- package/src/helpers/response-headers/environment.types.ts +1 -0
- package/src/helpers/response-headers/resolve-environment.ts +17 -0
- package/src/helpers/slugify.test.ts +77 -0
- package/src/helpers/slugify.ts +34 -0
- package/src/index.ts +46 -0
- package/src/normalization/normalize-list.test.ts +43 -0
- package/src/normalization/normalize-list.ts +21 -0
- package/src/normalization/normalize-location.test.ts +91 -0
- package/src/normalization/normalize-location.ts +29 -0
- package/src/primitives/coordinate-precision.test.ts +46 -0
- package/src/primitives/coordinate-precision.ts +30 -0
- package/src/primitives/geo-point.schema.test.ts +70 -0
- package/src/primitives/geo-point.schema.ts +14 -0
- package/src/primitives/geoname-id.schema.test.ts +60 -0
- package/src/primitives/geoname-id.schema.ts +12 -0
- package/src/primitives/international-zip.schema.test.ts +212 -0
- package/src/primitives/international-zip.schema.ts +103 -0
- package/src/primitives/latitude.schema.test.ts +77 -0
- package/src/primitives/latitude.schema.ts +20 -0
- package/src/primitives/location.schema.test.ts +21 -0
- package/src/primitives/location.schema.ts +22 -0
- package/src/primitives/longitude.schema.test.ts +77 -0
- package/src/primitives/longitude.schema.ts +20 -0
- package/src/primitives/numeric-id.schema.test.ts +32 -0
- package/src/primitives/numeric-id.schema.ts +13 -0
- package/src/primitives/slug.schema.test.ts +101 -0
- package/src/primitives/slug.schema.ts +41 -0
- package/src/primitives/uuid.schema.test.ts +45 -0
- package/src/primitives/uuid.schema.ts +12 -0
- package/src/primitives/wikidata-id.schema.test.ts +51 -0
- package/src/primitives/wikidata-id.schema.ts +16 -0
- package/src/time/README.md +220 -0
- package/src/time/boundary-enforcement.test.ts +130 -0
- package/src/time/boundary-enforcement.ts +59 -0
- package/src/time/bounded-time.schema.test.ts +294 -0
- package/src/time/bounded-time.schema.ts +111 -0
- package/src/time/flexible-time-parser.test.ts +586 -0
- package/src/time/flexible-time-parser.ts +122 -0
- package/src/time/flexible-time.schema.test.ts +243 -0
- package/src/time/flexible-time.schema.ts +43 -0
- package/src/time/is-relative-time.test.ts +23 -0
- package/src/time/is-relative-time.ts +9 -0
- package/src/time/iso8601.schema.ts +29 -0
- package/src/time/iso8601.types.test.ts +112 -0
- package/src/time/iso8601.types.ts +21 -0
- package/src/time/parse-relative-time.test.ts +49 -0
- package/src/time/parse-relative-time.ts +50 -0
- package/src/time/relative-time.schema.test.ts +23 -0
- package/src/time/relative-time.schema.ts +38 -0
- package/src/time/since-parameter.schema.test.ts +59 -0
- package/src/time/since-parameter.schema.ts +69 -0
- package/src/time/time-helpers.test.ts +263 -0
- package/src/time/time-helpers.ts +78 -0
- package/src/time/time-schemas.test.ts +181 -0
- package/src/time/time-schemas.ts +42 -0
- package/src/time/time.schema.test.ts +237 -0
- package/src/time/timezone-independence.test.ts +188 -0
- package/src/time/timezone.types.test.ts +55 -0
- package/src/time/timezone.types.ts +22 -0
- 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 @@
|
|
|
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
|
+
});
|