@revolugo/common 6.14.4 → 6.14.5-beta.0

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/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@revolugo/common",
3
- "version": "6.14.4",
3
+ "version": "6.14.5-beta.0",
4
4
  "private": false,
5
5
  "description": "Revolugo common",
6
6
  "author": "Revolugo",
7
7
  "type": "module",
8
+ "engines": {
9
+ "node": ">=20.18.1 <25"
10
+ },
11
+ "volta": {
12
+ "extends": "../../package.json"
13
+ },
8
14
  "files": [
9
15
  "src"
10
16
  ],
@@ -17,6 +23,7 @@
17
23
  "./icons": "./src/icons/index.ts",
18
24
  "./map": "./src/map/index.ts",
19
25
  "./models": "./src/models/index.ts",
26
+ "./schemas": "./src/schemas/index.ts",
20
27
  "./types": "./src/types/index.ts",
21
28
  "./utils": "./src/utils/index.ts"
22
29
  },
@@ -25,13 +32,9 @@
25
32
  "dayjs": "1.11.19",
26
33
  "ky": "1.14.0",
27
34
  "slugify": "1.6.6",
28
- "uuid": "13.0.0"
29
- },
30
- "engines": {
31
- "node": ">=20.18.1 <25"
32
- },
33
- "volta": {
34
- "extends": "../../package.json"
35
+ "type-fest": "5.3.0",
36
+ "uuid": "13.0.0",
37
+ "zod": "3.25.76"
35
38
  },
36
39
  "scripts": {
37
40
  "test": "vitest"
@@ -0,0 +1,11 @@
1
+ export enum HotelRoomOfferType {
2
+ HotelRoom = 'HOTEL_ROOM',
3
+ Package = 'PACKAGE',
4
+ }
5
+
6
+ export enum PackageType {
7
+ BestMatch = 'BEST_MATCH',
8
+ Cheapest = 'CHEAPEST',
9
+ MatchingRoomCount = 'MATCHING_ROOM_COUNT',
10
+ Regular = 'REGULAR',
11
+ }
@@ -1,12 +1,13 @@
1
1
  export * from '../countries/constants.ts'
2
2
  export * from '../currencies/constants.ts'
3
-
4
3
  export * from './environment.ts'
5
4
  export * from './hotel.ts'
6
5
  export * from './hotel-offers.ts'
6
+ export * from './hotel-room-offer.ts'
7
7
  export * from './locales.ts'
8
8
  export * from './measurement.ts'
9
9
  export * from './poller.ts'
10
+ export * from './tax.ts'
10
11
  export * from './time.ts'
11
12
  export * from './stay-taxes-info.ts'
12
13
  export * from './venue.ts'
@@ -33,11 +33,11 @@ export function langFromString(langStr: string): Lang | undefined {
33
33
  export const LANG_TO_LOCALE: Record<Lang, Locale> = {
34
34
  [Lang.EN]: Locale.en_US,
35
35
  [Lang.FR]: Locale.fr_FR,
36
- [Lang.DE]: Locale.en_US,
37
- [Lang.ES]: Locale.en_US,
38
- [Lang.IT]: Locale.en_US,
39
- [Lang.NL]: Locale.en_US,
40
- [Lang.PT]: Locale.en_US,
36
+ [Lang.DE]: Locale.de_DE,
37
+ [Lang.ES]: Locale.es_ES,
38
+ [Lang.IT]: Locale.it_IT,
39
+ [Lang.NL]: Locale.nl_NL,
40
+ [Lang.PT]: Locale.pt_PT,
41
41
  }
42
42
 
43
43
  /* @__PURE__ */
@@ -0,0 +1,9 @@
1
+ export enum TaxFrequency {
2
+ PerNight = 'PER_NIGHT',
3
+ PerStay = 'PER_STAY',
4
+ }
5
+
6
+ export enum TaxMode {
7
+ PerAdult = 'PER_ADULT',
8
+ PerBooking = 'PER_BOOKING',
9
+ }
@@ -85,7 +85,6 @@ export const ICONS_NAME = Object.freeze({
85
85
  gear: 'ph:gear',
86
86
  globe: 'ph:globe',
87
87
  golf: 'ph:golf',
88
- googleColored: 'flat-color-icons:google',
89
88
  graph: 'ph:graph',
90
89
  gym: 'ph:barbell',
91
90
  hairDryer: 'ph:hair-dryer',
@@ -0,0 +1,18 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ export const CANCELLATION_POLICY_SCHEMA = z.object({
5
+ date_from: z.string().openapi({
6
+ description:
7
+ 'The start date and time of the cancellation policy, given in the hotel timezone.',
8
+ }),
9
+ date_to: z.string().openapi({
10
+ description:
11
+ 'The end date and time of the cancellation policy, given in the hotel timezone.',
12
+ }),
13
+ penalty_percentage: z.number().min(0).max(100).openapi({
14
+ description:
15
+ 'The penalty percentage that is due in case of cancellation during the **date_from** to **date_to** period range.',
16
+ }),
17
+ })
18
+ /* eslint-enable camelcase */
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod'
2
+
3
+ import { STRIPE_CURRENCY_CODES } from '../currencies/index.ts'
4
+
5
+ export const CURRENCY_SCHEMA = z
6
+ .enum(STRIPE_CURRENCY_CODES)
7
+ .openapi({ description: 'ISO 4217 currency code.' })
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod'
2
+
3
+ import { CountryIso2Code } from '../constants/index.ts'
4
+
5
+ export const ADULT_COUNT_SCHEMA = z.number().int().min(1).max(200).openapi({
6
+ description:
7
+ 'The total number of adults who will be staying in the property.',
8
+ })
9
+
10
+ export const CHECK_IN_DATE_SCHEMA = z
11
+ .string()
12
+ .regex(/^(19|20)\d\d[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$/u)
13
+ .openapi({ description: 'Date of check-in formatted as YYYY-MM-DD.' })
14
+
15
+ export const CHECK_OUT_DATE_SCHEMA = z
16
+ .string()
17
+ .regex(/^(19|20)\d\d[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$/u)
18
+ .openapi({ description: 'Date of check-out formatted as YYYY-MM-DD.' })
19
+
20
+ export const CHILDREN_SCHEMA = z
21
+ .string()
22
+ .regex(/^(([0-9]{1})|(1[0-7]){1})(,(([0-9]{1})|(1[0-7]){1}))*$|^$/u)
23
+ .openapi({
24
+ description:
25
+ 'A comma-separated list of child ages (0 up to 17). e.g.: "3,7" represents 2 children respectively 3 and 7 years old.',
26
+ })
27
+
28
+ export const COUNTRY_ISO2_CODE_SCHEMA = z
29
+ .nativeEnum(CountryIso2Code)
30
+ .refine(check => Object.values(CountryIso2Code).includes(check), {
31
+ message: 'Invalid ISO Alpha-2 country code.',
32
+ })
33
+ .openapi({
34
+ description: 'ISO Alpha-2 country code.',
35
+ })
36
+
37
+ export const SOURCE_MARKET_SCHEMA = COUNTRY_ISO2_CODE_SCHEMA.openapi(
38
+ 'sourceMarket',
39
+ {
40
+ description:
41
+ 'For sourcing availability within certain markets, a source market option may be used to get more accurate prices. You may use any valid ISO Alpha-2 country code, e.g. JP.',
42
+ },
43
+ )
@@ -0,0 +1,58 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ import { CURRENCY_SCHEMA } from './currency.ts'
5
+ import {
6
+ ADULT_COUNT_SCHEMA,
7
+ CHECK_IN_DATE_SCHEMA,
8
+ CHECK_OUT_DATE_SCHEMA,
9
+ CHILDREN_SCHEMA,
10
+ } from './hotel-offer-request.ts'
11
+ import { HOTEL_ROOM_OFFER_SCHEMA } from './hotel-room-offer.ts'
12
+ import { HOTEL_IMAGES, HOTEL_SCHEMA, VENUES_SCHEMA } from './hotel.ts'
13
+ import { LIST_POLLING_META_SCHEMA } from './list-polling-meta.ts'
14
+ import { TAGS_SCHEMA } from './tag.ts'
15
+
16
+ export const HOTEL_OFFER_SCHEMA = HOTEL_SCHEMA.extend({
17
+ hotel_images: HOTEL_IMAGES.openapi({
18
+ description: `⚠️ Cached images of the hotel. May be empty, use the endpoint /hotels/{id}/images to get the latest images.
19
+
20
+ List of hotel images in various sizes featuring an indicator for the primary (hero) image
21
+ `,
22
+ }),
23
+ hotel_room_offers: z.array(HOTEL_ROOM_OFFER_SCHEMA),
24
+ tags: TAGS_SCHEMA,
25
+ venues: VENUES_SCHEMA,
26
+ })
27
+
28
+ export const HOTEL_OFFERS_SCHEMA = z.array(HOTEL_OFFER_SCHEMA)
29
+
30
+ export const HOTEL_OFFERS_RESPONSE_SCHEMA = z.object({
31
+ data: z.object({
32
+ adult_count: ADULT_COUNT_SCHEMA,
33
+ check_in_date: CHECK_IN_DATE_SCHEMA,
34
+ check_out_date: CHECK_OUT_DATE_SCHEMA,
35
+ children: CHILDREN_SCHEMA.optional().nullish(),
36
+ currency: CURRENCY_SCHEMA,
37
+ hotel_offers: HOTEL_OFFERS_SCHEMA.openapi({
38
+ description: 'List of Hotel Offers',
39
+ }),
40
+ price_histogram: z
41
+ .array(z.number())
42
+ .openapi({
43
+ description:
44
+ '**Hotel Offers** price histogram dataset based on the price of the cheapest **Hotel Room Offer** included on each **Hotel Offer** returned. It represents the number of available **Hotel Offers** grouped by price sorted ascendingly. Each item of the list represents a price step based on returned **price_min**, **price_max** and requested **price_histogram_step_count**',
45
+ })
46
+ .nullish(),
47
+ price_max: z.number().nullish().openapi({
48
+ description: 'Maximum price of available returned **Hotel Offers**',
49
+ }),
50
+ price_min: z.number().nullish().openapi({
51
+ description: 'Minimum price of available returned **Hotel Offers**',
52
+ }),
53
+ }),
54
+ event: z.any().optional(),
55
+ meta: LIST_POLLING_META_SCHEMA.extend({
56
+ total_count: z.number(),
57
+ }),
58
+ })
@@ -0,0 +1,99 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ import {
5
+ BreakfastOption,
6
+ HotelRoomOfferType,
7
+ PackageType,
8
+ } from '../constants/index.ts'
9
+
10
+ import { CANCELLATION_POLICY_SCHEMA } from './cancellation-policies.ts'
11
+ import { CURRENCY_SCHEMA } from './currency.ts'
12
+ import { SOURCE_MARKET_SCHEMA } from './hotel-offer-request.ts'
13
+ import { HOTEL_ROOMS_SCHEMA } from './hotel-room.ts'
14
+ import { TAGS_SCHEMA } from './tag.ts'
15
+ import { TAXES_SCHEMA } from './taxes.ts'
16
+
17
+ export const BREAKFAST_OPTION_DESCRIPTION = `This parameter describes the breakfast option for the given **Hotel Room Offer**:
18
+ - **breakfast_option = "${BreakfastOption.Included}"**:
19
+ 1. When **HotelRoomOffer.type = "${HotelRoomOfferType.Package}"**:
20
+ the returned **Hotel Room Offer** includes breakfast for the requested **guest count (adult_count + children over 3)**, you cannot choose otherwise, and the returned **price** already includes it,
21
+ 2. When **HotelRoomOffer.type = "${HotelRoomOfferType.HotelRoom}"**:
22
+ the returned **Hotel Room Offer** includes breakfast for the **Hotel Room Offer** maximum occupancy, you cannot choose otherwise, and the returned **price** already includes it,
23
+ -**breakfast_option = "${BreakfastOption.Optional}"**: the returned **Hotel Room Offer** does not include by default the breakfast and so does the returned **price**, but you'll be able to let your customers choose to add it to their booking. In that case, a **breakfast_price_per_guest_per_night** expressed in the requested **currency** will be available in the returned data, and you'll be to perform one of the two following actions: \n - Call **[Create Hotel Room Offer endpoint](/v1/documentation#operation/postV1Hotel_room_offers)** and get a fresh **Hotel Room Offer** with updated price \n - Compute and display the total price of the **Hotel Room Offer** related to the guest_count and night count requested including extra breafasts.,
24
+ - **breakfast_option = "${BreakfastOption.NotIncluded}"**: the returned **Hotel Room Offer** does not include breakfast and you cannot choose otherwise through API. Guest may still be able to add extra breakfast option(s) at the time of check-in directly at the hotel's reception.`
25
+
26
+ export const PACKAGE_TYPES_DESCRIPTION = `An **Hotel Room Offer** of type **${HotelRoomOfferType.Package}** can be of **4 types**, described by **package_type** parameter: \n\n - **${PackageType.Cheapest}** : The cheapest combination of hotel rooms that can accommodate the requested guest count. Note that it may not match the requested room count (e.g., 4 guests and 2 rooms requested may return an hotel room package including only 1 room with an occupancy of 4). \n\n -**${PackageType.MatchingRoomCount}** : the cheapest hotel room package that can accommodate the given guest count and that matches exactly the room count given. \n\n -**${PackageType.BestMatch}** : The cheapest hotel room package that matches the given room and guest count with a balanced distribution of guests across the rooms (e.g.; 8 guests and 3 rooms requested may return an **Hotel Room Offer** package including 2 rooms with an occupancy of 3 on each one and 1 room with an occupancy of 2). \n\n -**${PackageType.Regular}** : any other available package.`
27
+
28
+ export const HOTEL_ROOM_OFFER_SCHEMA = z
29
+ .object({
30
+ breakfast_count: z.number().min(0).optional().nullish().openapi({
31
+ description:
32
+ 'Quantity of breakfast per night included in the given **Hotel Room Offer**',
33
+ }),
34
+ breakfast_option: z.nativeEnum(BreakfastOption).openapi({
35
+ description: BREAKFAST_OPTION_DESCRIPTION,
36
+ }),
37
+ breakfast_price_per_guest_per_night: z
38
+ .number()
39
+ .optional()
40
+ .nullish()
41
+ .openapi({
42
+ description:
43
+ 'Price of breakfast per guest per night for the given **Hotel Room Offer**, expressed in the requested **currency**, when applicable.\n\n <div style="background-color: #ffffef; padding: 20px; border: 1px solid lightgrey; border-radius: 5px;"><b style="color: red; margin-top: 10px;">🛑 DEPRECATED.</b>\n\n <b style="color: orange;">Field renamed to "breakfast_unit_price"</div>',
44
+ }),
45
+ breakfast_unit_price: z.number().optional().nullish().openapi({
46
+ description:
47
+ 'Price of breakfast per guest per night for the given **Hotel Room Offer**, expressed in the requested **currency**, when applicable.',
48
+ }),
49
+ cancellation_policies: z
50
+ .array(CANCELLATION_POLICY_SCHEMA)
51
+ .openapi('cancellationPoliciesApi', {
52
+ description:
53
+ 'The list of cancellation policies applied to the given **Hotel Room Offer**.',
54
+ }),
55
+ count: z
56
+ .number()
57
+ .optional()
58
+ .nullish()
59
+ .openapi({
60
+ description: `When **type = ${HotelRoomOfferType.HotelRoom}**: this parameters represents the available quantity for the given **Hotel Room Offer**.
61
+ When **type = ${HotelRoomOfferType.Package}**: count = 1 always.`,
62
+ }),
63
+ currency: CURRENCY_SCHEMA,
64
+ hotel_id: z
65
+ .string()
66
+ .openapi({ description: 'id of the associated Hotel.' }),
67
+ hotel_rooms: HOTEL_ROOMS_SCHEMA.openapi({
68
+ description: 'List of Hotel Rooms included in the Hotel Room Offer.',
69
+ }),
70
+ id: z.string().openapi({ description: 'Hotel Room Offer id.' }).optional(),
71
+ package_type: z
72
+ .nativeEnum(PackageType)
73
+ .openapi({
74
+ description: PACKAGE_TYPES_DESCRIPTION,
75
+ })
76
+ .nullish()
77
+ .optional(),
78
+ price: z.number().openapi({
79
+ description:
80
+ 'Price with taxes NOT INCLUDED of the given **Hotel Room Offer** including breakfast(s) when applicable, expressed in the requested **currency**.',
81
+ }),
82
+ source_market: SOURCE_MARKET_SCHEMA,
83
+ tags: TAGS_SCHEMA,
84
+ tax_included_price: z.number().openapi({
85
+ description: `Price of the given **Hotel Room Offer** including breakfast(s) when applicable, and including all taxes from returned **taxes** list parameter expressed in the requested **currency**.\n\nThis data is not returned for a **Hotel Room Offer** of type **${HotelRoomOfferType.HotelRoom}**, you'll need to compute and display the actual tax included price of the final **Hotel Room** package corresponding to the guest count and night count requested, or make a call to the **[Create Hotel Room Offer](/v1/documentation#operation/postV1Hotel_room_offers)** endpoint in order to get a valid and bookable **Hotel Room Offer** where **type = ${HotelRoomOfferType.Package}** based on a packaged list of **Hotel Room Offers** of type **${HotelRoomOfferType.HotelRoom}**.`,
86
+ }),
87
+ taxes: TAXES_SCHEMA,
88
+ type: z
89
+ .nativeEnum(HotelRoomOfferType)
90
+ .openapi({
91
+ description: `Hotel Room Offer type.\n\n **Hotel Room Offers** with **type = "${HotelRoomOfferType.Package}"** are **Hotel Room Offers** that are already bookable and you'll be able to follow the next step of the **Booking Flow** calling **[Create Booking Policies endpoint](/v1/documentation#operation/postV1Booking_policies)**. \n\n Otherwise, you'll be able to create a new **Hotel Room Offer** with **type = "${HotelRoomOfferType.Package}"** from multiple **Hotel Room Offers** with **type = "${HotelRoomOfferType.HotelRoom}"**. See **[Create Hotel Room Offer endpoint](/v1/documentation#operation/postV1Hotel_room_offers)** for details.`,
92
+ })
93
+ .nullish()
94
+ .optional(),
95
+ })
96
+ .openapi('hotelRoomOfferApi', {
97
+ description: 'Description of the Hotel Room Offer.',
98
+ })
99
+ /* eslint-enable camelcase */
@@ -0,0 +1,106 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ import { HOTEL_IMAGE } from './hotel.ts'
5
+
6
+ export const BED_SCHEMA = z
7
+ .object({
8
+ count: z.number().openapi({
9
+ description: 'Number of beds of the given type in the room.',
10
+ }),
11
+ name: z.string().openapi({ description: 'Bed name.' }),
12
+ occupancy: z.number().openapi({ description: 'Bed occupancy.' }),
13
+ })
14
+ .openapi('bedApi')
15
+
16
+ export const BEDS_SCHEMA = z
17
+ .array(z.array(BED_SCHEMA))
18
+ .openapi({
19
+ description: `Beds list.
20
+ Each nested array of beds represents a single combination of possible beds.
21
+ e.g.: The following object represents **1 double bed or 1 sofa bed and 1 double bed or 1 single bed**:
22
+ \`\`\`
23
+ [
24
+ [
25
+ { count: 1, name: 'double', occupancy: 2 },
26
+ { count: 1, name: 'sofa', occupancy: 1 }
27
+ ],
28
+ [
29
+ { count: 1, name: 'double', occupancy: 2 },
30
+ { count: 1, name: 'single', occupancy: 1 }
31
+ ]
32
+ ]
33
+ \`\`\`
34
+ `,
35
+ })
36
+ .openapi('bedsApi')
37
+
38
+ export const HOTEL_ROOM_SCHEMA = z
39
+ .object({
40
+ amenities: z
41
+ .array(z.string().openapi('amenity'))
42
+ .openapi({
43
+ description:
44
+ 'List of amenities in the room. May be subject to changes at the Hotel.',
45
+ })
46
+ .nullish(),
47
+ beds: BEDS_SCHEMA,
48
+ beds_pretty: z
49
+ .string()
50
+ .openapi({ description: 'Prettified and localized list of beds' }),
51
+ count: z.number().openapi({
52
+ description: 'Hotel Room count included in the Hotel Room Offer.',
53
+ }),
54
+ description: z.string().openapi({ description: 'Hotel Room description.' }),
55
+ highres_images: z
56
+ .boolean()
57
+ .openapi({ description: 'Whether high resolution images are available.' })
58
+ .optional()
59
+ .nullish(),
60
+ id: z
61
+ .string()
62
+ .optional()
63
+ .openapi({ description: 'Hotel Room id, when applicable.' }),
64
+ image_indexes: z.array(z.number()).optional().nullish().openapi({
65
+ description:
66
+ 'List of indexes corresponding to image names for the given Hotel Room among the related hotel images.',
67
+ }),
68
+ images: z
69
+ .array(HOTEL_IMAGE)
70
+ .openapi({
71
+ description: 'Hotel Room images.',
72
+ })
73
+ .optional()
74
+ .nullish(),
75
+ lowres_images: z
76
+ .boolean()
77
+ .openapi({ description: 'Whether low resolution images are available.' })
78
+ .optional()
79
+ .nullish(),
80
+ occupancy: z
81
+ .number()
82
+ .openapi({ description: 'Total occupancy of a single hotel room.' }),
83
+ rich_description: z.string().optional().nullish().openapi({
84
+ description: 'Rich Hotel Room description. May contain HTML tags markup.',
85
+ }),
86
+ room_square_feet: z.number().optional().nullish().openapi({
87
+ description:
88
+ 'Room surface in square feet. May be subject to changes at the Hotel.',
89
+ }),
90
+ room_square_meters: z.number().optional().nullish().openapi({
91
+ description:
92
+ 'Room surface in square meters. May be subject to changes at the Hotel.',
93
+ }),
94
+ thumb_images: z
95
+ .boolean()
96
+ .openapi({
97
+ description:
98
+ 'Whether thumb resolution images are available (in order to display them as image carousel navigation for instance).',
99
+ })
100
+ .optional()
101
+ .nullish(),
102
+ })
103
+ .openapi({ description: 'Hotel Room details.' })
104
+
105
+ export const HOTEL_ROOMS_SCHEMA = z.array(HOTEL_ROOM_SCHEMA)
106
+ /* eslint-enable camelcase */
@@ -0,0 +1,360 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ import { CountryIso2Code } from '../constants/index.ts'
5
+
6
+ import { CURRENCY_SCHEMA } from './currency.ts'
7
+
8
+ export const AMENITIES_SCHEMA = z.object({
9
+ air_conditioning: z
10
+ .boolean()
11
+ .optional()
12
+ .openapi({ description: 'Air conditioning.' }),
13
+ airport_transportation: z
14
+ .boolean()
15
+ .optional()
16
+ .openapi({ description: 'Airport transportation service.' }),
17
+ business_center: z
18
+ .boolean()
19
+ .optional()
20
+ .openapi({ description: 'Business center.' }),
21
+ car_rent_desk: z
22
+ .boolean()
23
+ .optional()
24
+ .openapi({ description: 'Car rental desk service.' }),
25
+ children_allowed: z
26
+ .boolean()
27
+ .optional()
28
+ .openapi({ description: 'Children welcomed.' }),
29
+ clothing_iron: z
30
+ .boolean()
31
+ .optional()
32
+ .openapi({ description: 'Clothing iron.' }),
33
+ coffee_tea_maker: z
34
+ .boolean()
35
+ .optional()
36
+ .openapi({ description: 'Coffea/tea maker.' }),
37
+ combination: z.boolean().optional().openapi({ description: 'Combination.' }),
38
+ continental_breakfast: z
39
+ .boolean()
40
+ .optional()
41
+ .openapi({ description: 'Continental Breakfast.' }),
42
+ data_ports: z
43
+ .boolean()
44
+ .optional()
45
+ .openapi({ description: 'Data ports in room.' }),
46
+ dry_cleaning: z
47
+ .boolean()
48
+ .optional()
49
+ .openapi({ description: 'Dry cleaning.' }),
50
+ electronic_room_keys: z
51
+ .boolean()
52
+ .optional()
53
+ .openapi({ description: 'Electornic room keys.' }),
54
+ exterior_room_entrance: z
55
+ .boolean()
56
+ .optional()
57
+ .openapi({ description: 'Exterior room entrance.' }),
58
+ family_rooms: z
59
+ .boolean()
60
+ .optional()
61
+ .openapi({ description: 'Family rooms.' }),
62
+ fitness_facility: z
63
+ .boolean()
64
+ .optional()
65
+ .openapi({ description: 'Fitness facility.' }),
66
+ game_room: z.boolean().optional().openapi({ description: 'Game room.' }),
67
+ golf_course: z.boolean().optional().openapi({ description: 'Golf course.' }),
68
+ hair_dryer: z.boolean().optional().openapi({ description: 'Hair dryer.' }),
69
+ handicap_accessible: z
70
+ .boolean()
71
+ .optional()
72
+ .openapi({ description: 'Handicap Accessible.' }),
73
+ in_house_bar: z
74
+ .boolean()
75
+ .optional()
76
+ .openapi({ description: 'In house bar.' }),
77
+ in_house_dining: z
78
+ .boolean()
79
+ .optional()
80
+ .openapi({ description: 'In house dining.' }),
81
+ in_room_movies: z
82
+ .boolean()
83
+ .optional()
84
+ .openapi({ description: 'In room movies.' }),
85
+ indoor_pool: z.boolean().optional().openapi({ description: 'Indoor pool.' }),
86
+ interior_room_entrance: z
87
+ .boolean()
88
+ .optional()
89
+ .openapi({ description: 'Interior room entrance.' }),
90
+ kitchen: z.boolean().optional().openapi({ description: 'Kitchen.' }),
91
+ map: z.boolean().optional().openapi({ description: 'Map.' }),
92
+ meeting_rooms: z
93
+ .boolean()
94
+ .optional()
95
+ .openapi({ description: 'Meeting rooms.' }),
96
+ mini_bar_in_room: z
97
+ .boolean()
98
+ .optional()
99
+ .openapi({ description: 'mini bar in room.' }),
100
+ non_smoking_rooms: z
101
+ .boolean()
102
+ .optional()
103
+ .openapi({ description: 'Non smoking rooms.' }),
104
+ outdoor_pool: z
105
+ .boolean()
106
+ .optional()
107
+ .openapi({ description: 'Outdoor pool.' }),
108
+ parking_garage: z
109
+ .boolean()
110
+ .optional()
111
+ .openapi({ description: 'Parking garage.' }),
112
+ pets_allowed: z
113
+ .boolean()
114
+ .optional()
115
+ .openapi({ description: 'Pets allowed.' }),
116
+ restricted_access: z
117
+ .boolean()
118
+ .optional()
119
+ .openapi({ description: 'Restricted access.' }),
120
+ room_service: z
121
+ .boolean()
122
+ .optional()
123
+ .openapi({ description: 'Room service.' }),
124
+ safe: z.boolean().optional().openapi({ description: 'Safe in room.' }),
125
+ sauna: z.boolean().optional().openapi({ description: 'Sauna.' }),
126
+ t_v_in_room: z.boolean().optional().openapi({ description: 'TV in room.' }),
127
+ tennis_court: z
128
+ .boolean()
129
+ .optional()
130
+ .openapi({ description: 'Tennis court.' }),
131
+ twenty_four_hour_security: z
132
+ .boolean()
133
+ .optional()
134
+ .openapi({ description: ' 24/7 security.' }),
135
+ valet_parking: z
136
+ .boolean()
137
+ .optional()
138
+ .openapi({ description: 'Valet parking.' }),
139
+ video_check_out: z
140
+ .boolean()
141
+ .optional()
142
+ .openapi({ description: 'Video check out.' }),
143
+ voice_mail: z.boolean().optional().openapi({ description: 'Voice mail.' }),
144
+ wake_up_service: z
145
+ .boolean()
146
+ .optional()
147
+ .openapi({ description: 'Wake up service.' }),
148
+ whirpool: z.boolean().optional().openapi({ description: 'Whirpool.' }),
149
+ })
150
+
151
+ export const HOTEL_IMAGE = z.object({
152
+ caption: z.string().nullish(),
153
+ is_hero_image: z.boolean(),
154
+ l: z.string(),
155
+ m: z.string(),
156
+ s: z.string(),
157
+ xl: z.string(),
158
+ xs: z.string(),
159
+ })
160
+
161
+ export const HOTEL_IMAGES = z.array(HOTEL_IMAGE).openapi({
162
+ description:
163
+ 'List of hotel images in various sizes featuring an indicator for the primary (hero) image',
164
+ })
165
+
166
+ export const HOTEL_REVIEW_RATING_SCHEMA = z
167
+ .object({
168
+ category: z
169
+ .string()
170
+ .openapi({
171
+ description: 'Category of the collected reviews for the Hotel.',
172
+ })
173
+ .nullish()
174
+ .optional(),
175
+ rating: z.number().openapi({
176
+ description: 'Rating of the collected review for the Hotel.',
177
+ }),
178
+ })
179
+ .openapi({
180
+ description: 'Review rating with category collected for the Hotel.',
181
+ })
182
+
183
+ export const HOTEL_REVIEW_RATINGS_SCHEMA = z
184
+ .array(HOTEL_REVIEW_RATING_SCHEMA)
185
+ .openapi({
186
+ description:
187
+ 'List of meta reviews (category and rating) that are summary of verified reviews collected across the web on the Hotel to help choose the best option.',
188
+ })
189
+
190
+ export const IMAGES_SCHEMA = z
191
+ .object({
192
+ count: z
193
+ .number()
194
+ .openapi({ description: 'Number of images.' })
195
+ .optional()
196
+ .nullish(),
197
+ highres: z
198
+ .boolean()
199
+ .openapi({ description: 'Whether images exist in highres format.' })
200
+ .optional()
201
+ .nullish(),
202
+
203
+ lowres: z
204
+ .boolean()
205
+ .openapi({ description: 'Whether images exist in lowres format.' })
206
+ .optional()
207
+ .nullish(),
208
+ prefix: z
209
+ .string()
210
+ .openapi({ description: 'Base URL for the images.' })
211
+ .optional()
212
+ .nullish(),
213
+ suffix: z
214
+ .string()
215
+ .openapi({
216
+ description:
217
+ 'This parameter usually represents the extension of the image (e.g.: .jpg, .png)',
218
+ })
219
+ .optional()
220
+ .nullish(),
221
+ thumb: z
222
+ .boolean()
223
+ .openapi({
224
+ description:
225
+ 'Whether images exist in thumb format (for thumbnails preview).',
226
+ })
227
+ .optional()
228
+ .nullish(),
229
+ })
230
+ .openapi({
231
+ description:
232
+ '🛑 DEPRECATED - Hotel images details.\n\nIn order to retrieve a specific image you need to construct the complete URL from the images parameters: **[images.prefix][highres|lowres|thumb]/[index]/[images.suffix]**. If **images.count = n**, then index is in [0...n-1] range.\n\ne.g.: https://s3.eu-west-3.amazonaws.com/revolugo/hotels/yhKY/images/highres/0.jpg',
233
+ })
234
+
235
+ export const VENUES_SCHEMA = z
236
+ .array(
237
+ z.object({
238
+ description: z.string().optional().nullish(),
239
+ name: z.string(),
240
+ travel_times: z
241
+ .object({
242
+ driving: z.number().optional(),
243
+ transit: z.number().optional(),
244
+ walking: z.number().optional(),
245
+ })
246
+ .optional()
247
+ .nullish(),
248
+ }),
249
+ )
250
+ .optional()
251
+ export const HOTEL_SCHEMA = z
252
+ .object({
253
+ address: z
254
+ .string()
255
+ .openapi({ description: 'Hotel address.' })
256
+ .optional()
257
+ .nullish(),
258
+ address2: z
259
+ .string()
260
+ .openapi({ description: 'Second part of hotel address.' })
261
+ .optional()
262
+ .nullish(),
263
+ amenities: AMENITIES_SCHEMA.nullish(),
264
+ check_in_time: z
265
+ .string()
266
+ .openapi({ description: 'Check in time of the hotel.' })
267
+ .optional()
268
+ .nullish(),
269
+ check_out_time: z
270
+ .string()
271
+ .openapi({ description: 'Check out time of the hotel.' })
272
+ .optional()
273
+ .nullish(),
274
+ city: z.string().openapi({ description: 'City' }).optional().nullish(),
275
+ country: z
276
+ .string()
277
+ .openapi({ description: 'Country' })
278
+ .optional()
279
+ .nullish(),
280
+ country_code: z
281
+ .string()
282
+ .refine(check =>
283
+ Object.values(CountryIso2Code).includes(check as CountryIso2Code),
284
+ )
285
+ .openapi({ description: 'Hotel country code in ISO2.' })
286
+ .optional()
287
+ .nullish(),
288
+ currency: CURRENCY_SCHEMA.openapi({ description: 'Hotel currency.' })
289
+ .optional()
290
+ .nullish(),
291
+ description: z
292
+ .string()
293
+ .openapi({ description: 'Hotel description.' })
294
+ .optional()
295
+ .nullish(),
296
+ distance: z
297
+ .number()
298
+ .optional()
299
+ .openapi({
300
+ description: 'Distance from a requested location, expressed in meters',
301
+ })
302
+ .optional()
303
+ .nullish(),
304
+ email: z
305
+ .string()
306
+ .openapi({ description: 'Hotel email.' })
307
+ .optional()
308
+ .nullish(),
309
+ fax: z
310
+ .string()
311
+ .openapi({ description: 'Hotel fax number.' })
312
+ .optional()
313
+ .nullish(),
314
+ hotel_images: HOTEL_IMAGES.nullish(),
315
+ hotel_review_ratings: HOTEL_REVIEW_RATINGS_SCHEMA.optional().nullish(),
316
+ id: z.string().openapi({ description: 'Hotel id.' }),
317
+ images: IMAGES_SCHEMA.nullish(),
318
+ latitude: z.number().openapi({ description: 'Hotel latitude.' }),
319
+ longitude: z.number().openapi({ description: 'Hotel longitude.' }),
320
+ name: z.string().openapi({ description: 'Hotel name.' }),
321
+ phone: z
322
+ .string()
323
+ .openapi({ description: 'Hotel phone number.' })
324
+ .optional()
325
+ .nullish(),
326
+ policy: z
327
+ .string()
328
+ .openapi({ description: 'Internal policy of the hotel.' })
329
+ .optional()
330
+ .nullish(),
331
+ postal_code: z
332
+ .string()
333
+ .openapi({ description: 'Hotel address postal code.' })
334
+ .optional()
335
+ .nullish(),
336
+ rating: z
337
+ .number()
338
+ .optional()
339
+ .nullish()
340
+ .openapi({ description: 'Hotel Star rating.' }),
341
+ state: z
342
+ .string()
343
+ .openapi({ description: 'Hotel address state.' })
344
+ .optional()
345
+ .nullish(),
346
+ ta_id: z
347
+ .string()
348
+ .optional()
349
+ .openapi({ description: 'TripAdvisor property id. When applicable.' })
350
+ .optional()
351
+ .nullish(),
352
+ timezone: z.string().openapi({ description: 'Hotel timezone.' }),
353
+ venues: VENUES_SCHEMA,
354
+ website: z
355
+ .string()
356
+ .openapi({ description: 'Hotel website url.' })
357
+ .optional()
358
+ .nullish(),
359
+ })
360
+ .openapi('hotelApi')
@@ -0,0 +1,10 @@
1
+ export * from './cancellation-policies.ts'
2
+ export * from './currency.ts'
3
+ export * from './hotel-offer-request.ts'
4
+ export * from './hotel-offer.ts'
5
+ export * from './hotel-room-offer.ts'
6
+ export * from './hotel-room.ts'
7
+ export * from './hotel.ts'
8
+ export * from './list-polling-meta.ts'
9
+ export * from './tag.ts'
10
+ export * from './taxes.ts'
@@ -0,0 +1,43 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ import { PollerStatus } from '@revolugo/common/constants'
5
+
6
+ export const ENDING_BEFORE_SCHEMA = z.string().optional().nullable().openapi({
7
+ description:
8
+ 'A cursor to use in pagination. `ending_before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with `obj_bar`, your subsequent call can include `ending_before=obj_bar` in order to fetch the previous page of the list.',
9
+ })
10
+
11
+ export const STARTING_AFTER_SCHEMA = z.string().optional().nullable().openapi({
12
+ description:
13
+ 'A cursor to use in pagination. `starting_after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with `obj_foo`, your subsequent call can include `starting_after=obj_foo` in order to fetch the next page of the list.',
14
+ })
15
+
16
+ export const STATUS_SCHEMA = z
17
+ .nativeEnum(PollerStatus)
18
+ .openapi({ description: 'Status of the response data.' })
19
+ .openapi('pollerStatus')
20
+
21
+ export const LIMIT_SCHEMA = z
22
+ .number()
23
+ .optional()
24
+ .openapi({ description: 'A limit on the number of object to be returned.' })
25
+ .openapi('limit')
26
+
27
+ export const LIST_META_SCHEMA = z
28
+ .object({
29
+ ending_before: ENDING_BEFORE_SCHEMA,
30
+ limit: LIMIT_SCHEMA,
31
+ starting_after: STARTING_AFTER_SCHEMA,
32
+ total_count: z.number().nullish(),
33
+ })
34
+ .openapi('metaApiResponse')
35
+ .openapi({
36
+ description:
37
+ 'Meta information about the response list, such as pagination cursors or status.',
38
+ })
39
+
40
+ export const LIST_POLLING_META_SCHEMA = LIST_META_SCHEMA.extend({
41
+ status: STATUS_SCHEMA,
42
+ }).openapi('metaApiPollingResponse')
43
+ /* eslint-enable camelcase */
@@ -0,0 +1,13 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ export const TAG_SCHEMA = z.object({
5
+ bg: z.string().optional().nullish(),
6
+ color: z.string().optional().nullish(),
7
+ description: z.string().optional().nullish(),
8
+ fa_icon: z.string().optional().nullish(),
9
+ name: z.string(),
10
+ })
11
+
12
+ export const TAGS_SCHEMA = z.array(TAG_SCHEMA).optional().default([])
13
+ /* eslint-enable camelcase */
@@ -0,0 +1,34 @@
1
+ /* eslint-disable camelcase */
2
+ import { z } from 'zod'
3
+
4
+ import { TaxFrequency, TaxMode } from '../constants/index.ts'
5
+
6
+ export const TAX_SCHEMA = z
7
+ .object({
8
+ amount: z.number().optional().nullish().openapi({
9
+ description: 'Tax amount expressed in the requested currency.',
10
+ }),
11
+ code: z.string().optional().openapi({ description: 'Tax code.' }),
12
+ description: z
13
+ .string()
14
+ .optional()
15
+ .nullish()
16
+ .openapi({ description: 'Tax description.' }),
17
+ percentage: z
18
+ .number()
19
+ .optional()
20
+ .nullish()
21
+ .openapi({ description: 'Tax percentage on the total amount.' }),
22
+ tax_frequency: z.nativeEnum(TaxFrequency).openapi({
23
+ description:
24
+ 'Tax frequency. Specifies if the tax applies per stay or per night',
25
+ }),
26
+ tax_mode: z.nativeEnum(TaxMode).openapi({
27
+ description:
28
+ 'Tax mode. Specifies if the tax applies per occupant, per booking or per room',
29
+ }),
30
+ })
31
+ .openapi('taxApi')
32
+
33
+ export const TAXES_SCHEMA = z.array(TAX_SCHEMA).optional()
34
+ /* eslint-enable camelcase */
@@ -151,6 +151,8 @@ export interface Hotel {
151
151
  */
152
152
  // oxlint-disable-next-line no-explicit-any
153
153
  rating?: any | null
154
+
155
+ reviewCount?: number | null
154
156
  /**
155
157
  * Hotel address state.
156
158
  * @type {string}
@@ -7,5 +7,6 @@ export enum Severity {
7
7
  Secondary = 'secondary',
8
8
  Tertiary = 'tertiary',
9
9
  Danger = 'danger',
10
+ Error = 'error',
10
11
  Contrast = 'contrast',
11
12
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from 'change-case'
8
8
  import slugify from 'slugify'
9
9
 
10
- function slugCase(input: string) {
10
+ function slugCase(input: string): string {
11
11
  return slugify(kebabCase(input), {
12
12
  lower: true,
13
13
  strict: true,
@@ -23,7 +23,7 @@ export enum CaseTransformer {
23
23
  Snake = 'snakeCase',
24
24
  }
25
25
 
26
- const CASE_TRANSORMERS_MAPPING = {
26
+ export const CASE_TRANSFORMERS_MAPPING = {
27
27
  [CaseTransformer.Camel]: camelCase,
28
28
  [CaseTransformer.Capital]: capitalCase,
29
29
  [CaseTransformer.Param]: kebabCase,
@@ -38,11 +38,11 @@ export function changeCase<T extends string | string[]>(
38
38
  ): T extends string ? string : string[] {
39
39
  if (Array.isArray(input)) {
40
40
  return input.map(item =>
41
- CASE_TRANSORMERS_MAPPING[toCase](item),
41
+ CASE_TRANSFORMERS_MAPPING[toCase](item),
42
42
  ) as T extends string ? string : string[]
43
43
  }
44
44
 
45
- return CASE_TRANSORMERS_MAPPING[toCase](input) as T extends string
45
+ return CASE_TRANSFORMERS_MAPPING[toCase](input) as T extends string
46
46
  ? string
47
47
  : string[]
48
48
  }
@@ -85,7 +85,7 @@ export function keysChangeCase<T>(
85
85
  const transformedKey =
86
86
  options?.exclude && matches(options.exclude, key)
87
87
  ? key
88
- : CASE_TRANSORMERS_MAPPING[toCase](key)
88
+ : CASE_TRANSFORMERS_MAPPING[toCase](key)
89
89
 
90
90
  result[transformedKey] = options.deep
91
91
  ? keysChangeCase(obj[key], toCase, options)
@@ -0,0 +1,29 @@
1
+ const MAX_ADULTS_PER_ROOM = 2
2
+
3
+ export function getSanitizedRoomCount({
4
+ adultCount,
5
+ maxAdultsPerRoom = MAX_ADULTS_PER_ROOM,
6
+ roomCount,
7
+ }: {
8
+ adultCount: number
9
+ roomCount: number
10
+ maxAdultsPerRoom?: number
11
+ }): number {
12
+ // Ensure at least enough rooms for each adult and at most MAX_ADULTS_PER_ROOM per room
13
+ if (!Number.isFinite(roomCount) || roomCount < 1) {
14
+ return adultCount
15
+ }
16
+
17
+ const minRooms = Math.ceil(adultCount / maxAdultsPerRoom)
18
+ const maxRooms = adultCount
19
+
20
+ if (roomCount <= minRooms) {
21
+ return minRooms
22
+ }
23
+
24
+ if (roomCount >= maxRooms) {
25
+ return maxRooms
26
+ }
27
+
28
+ return roomCount
29
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './add-classes.ts'
2
2
  export * from './amount-from-percentage.ts'
3
3
  export * from './case-transformers.ts'
4
+ export * from './keys-case-transformer.ts'
4
5
  export * from './chunk.ts'
5
6
  export * from './colors.ts'
6
7
  export * from './compact-object.ts'
@@ -20,11 +21,13 @@ export * from './get-guest-count.ts'
20
21
  export * from './get-random-element-from-array.ts'
21
22
  export * from './get-random-hex-color.ts'
22
23
  export * from './get-random-int.ts'
24
+ export * from './get-sanitized-room-count.ts'
23
25
  export * from './group-by.ts'
24
26
  export * from './images.ts'
25
27
  export * from './is-empty.ts'
26
28
  export * from './is-equal.ts'
27
29
  export * from './is-nil.ts'
30
+ export * from './is-object.ts'
28
31
  export * from './key-by.ts'
29
32
  export * from './lang-default-fallbacks.ts'
30
33
  export * from './map-keys.ts'
@@ -45,6 +48,7 @@ export * from './sort-by.ts'
45
48
  export * from './sum-by.ts'
46
49
  export * from './sum.ts'
47
50
  export * from './to-boolean.ts'
51
+ export * from './transform-schema-keys.ts'
48
52
  export * from './uniq.ts'
49
53
  export * from './uniq-by.ts'
50
54
  export * from './uniq-with.ts'
@@ -0,0 +1,2 @@
1
+ export const isObject = (value: unknown): value is object =>
2
+ !!value && value.constructor === Object
@@ -0,0 +1,118 @@
1
+ import { type CaseTransformer, changeCase } from './case-transformers.ts'
2
+ import { isObject } from './is-object.ts'
3
+ import { mapKeys } from './map-keys.ts'
4
+
5
+ import type {
6
+ CamelCasedProperties,
7
+ CamelCasedPropertiesDeep,
8
+ KebabCasedProperties,
9
+ KebabCasedPropertiesDeep,
10
+ PascalCasedProperties,
11
+ PascalCasedPropertiesDeep,
12
+ SnakeCasedProperties,
13
+ SnakeCasedPropertiesDeep,
14
+ } from 'type-fest'
15
+
16
+ /**
17
+ * Maps CaseTransformer enum to the corresponding type-fest property transformation (shallow).
18
+ * Capital and Slug cases don't have type-fest equivalents, so they preserve the original type.
19
+ */
20
+ type TransformProperties<
21
+ T,
22
+ C extends CaseTransformer,
23
+ > = C extends CaseTransformer.Camel
24
+ ? CamelCasedProperties<T>
25
+ : C extends CaseTransformer.Snake
26
+ ? SnakeCasedProperties<T>
27
+ : C extends CaseTransformer.Pascal
28
+ ? PascalCasedProperties<T>
29
+ : C extends CaseTransformer.Param
30
+ ? KebabCasedProperties<T>
31
+ : T
32
+
33
+ /**
34
+ * Maps CaseTransformer enum to the corresponding type-fest deep property transformation.
35
+ * Capital and Slug cases don't have type-fest equivalents, so they preserve the original type.
36
+ */
37
+ type TransformPropertiesDeep<
38
+ T,
39
+ C extends CaseTransformer,
40
+ > = C extends CaseTransformer.Camel
41
+ ? CamelCasedPropertiesDeep<T>
42
+ : C extends CaseTransformer.Snake
43
+ ? SnakeCasedPropertiesDeep<T>
44
+ : C extends CaseTransformer.Pascal
45
+ ? PascalCasedPropertiesDeep<T>
46
+ : C extends CaseTransformer.Param
47
+ ? KebabCasedPropertiesDeep<T>
48
+ : T
49
+
50
+ /**
51
+ * Transforms object keys based on the specified case transformer.
52
+ * - Arrays: recursively applies transformation to each element
53
+ * - Objects: applies property transformation (shallow by default, deep if Deep=true)
54
+ * - Primitives: returns as-is
55
+ */
56
+ export type KeysCaseTransformed<
57
+ T,
58
+ C extends CaseTransformer,
59
+ Deep extends boolean = false,
60
+ > = Deep extends true
61
+ ? T extends readonly (infer U)[]
62
+ ? KeysCaseTransformed<U, C, true>[]
63
+ : T extends object
64
+ ? TransformPropertiesDeep<T, C>
65
+ : T
66
+ : T extends readonly (infer U)[]
67
+ ? KeysCaseTransformed<U, C>[]
68
+ : T extends object
69
+ ? TransformProperties<T, C>
70
+ : T
71
+
72
+ export interface KeysCaseTransformerOptions {
73
+ deep?: boolean
74
+ }
75
+
76
+ // Overload: deep transformation
77
+ export function keysCaseTransformer<T, C extends CaseTransformer>(
78
+ obj: T,
79
+ toCase: C,
80
+ options: { deep: true },
81
+ ): KeysCaseTransformed<T, C, true>
82
+
83
+ // Overload: shallow transformation (default)
84
+ export function keysCaseTransformer<T, C extends CaseTransformer>(
85
+ obj: T,
86
+ toCase: C,
87
+ options?: { deep?: false },
88
+ ): KeysCaseTransformed<T, C>
89
+
90
+ // Implementation
91
+ export function keysCaseTransformer<T, C extends CaseTransformer>(
92
+ obj: T,
93
+ toCase: C,
94
+ options?: KeysCaseTransformerOptions,
95
+ ): KeysCaseTransformed<T, C, boolean> {
96
+ if (Array.isArray(obj)) {
97
+ return obj.map(item =>
98
+ keysCaseTransformer(item, toCase, options as { deep: true }),
99
+ ) as KeysCaseTransformed<T, C, boolean>
100
+ }
101
+
102
+ if (isObject(obj)) {
103
+ const transformed = mapKeys(obj, key => changeCase(key, toCase))
104
+ if (options?.deep) {
105
+ // Recursively transform nested values
106
+ return Object.fromEntries(
107
+ Object.entries(transformed).map(([k, v]) => [
108
+ k,
109
+ keysCaseTransformer(v, toCase, options as { deep: true }),
110
+ ]),
111
+ ) as KeysCaseTransformed<T, C, boolean>
112
+ }
113
+
114
+ return transformed as KeysCaseTransformed<T, C, boolean>
115
+ }
116
+
117
+ return obj as KeysCaseTransformed<T, C, boolean>
118
+ }
@@ -0,0 +1,143 @@
1
+ import { type ZodTypeAny, z } from 'zod'
2
+
3
+ import {
4
+ CASE_TRANSFORMERS_MAPPING,
5
+ type CaseTransformer,
6
+ } from './case-transformers.ts'
7
+
8
+ /* eslint-disable no-underscore-dangle, max-statements */
9
+
10
+ function transformSchemaKeysWithTransformer(
11
+ schema: ZodTypeAny,
12
+ transformer: (str: string) => string,
13
+ ): ZodTypeAny {
14
+ let result: ZodTypeAny = schema
15
+
16
+ // Handle ZodObject
17
+ if (schema instanceof z.ZodObject) {
18
+ const shape = schema._def.shape()
19
+ const transformedShape: Record<string, ZodTypeAny> = {}
20
+
21
+ for (const key in shape) {
22
+ if (Object.hasOwn(shape, key)) {
23
+ const transformedKey = transformer(key)
24
+ transformedShape[transformedKey] = transformSchemaKeysWithTransformer(
25
+ shape[key],
26
+ transformer,
27
+ )
28
+ }
29
+ }
30
+
31
+ result = z.object(transformedShape) as ZodTypeAny
32
+ }
33
+
34
+ // Handle ZodArray
35
+ else if (schema instanceof z.ZodArray) {
36
+ const { type: elementSchema } = schema._def
37
+ result = z.array(
38
+ transformSchemaKeysWithTransformer(elementSchema, transformer),
39
+ )
40
+ }
41
+
42
+ // Handle ZodOptional
43
+ else if (schema instanceof z.ZodOptional) {
44
+ const { innerType: innerSchema } = schema._def
45
+ result = transformSchemaKeysWithTransformer(
46
+ innerSchema,
47
+ transformer,
48
+ ).optional()
49
+ }
50
+
51
+ // Handle ZodNullable
52
+ else if (schema instanceof z.ZodNullable) {
53
+ const { innerType: innerSchema } = schema._def
54
+ result = transformSchemaKeysWithTransformer(
55
+ innerSchema,
56
+ transformer,
57
+ ).nullable()
58
+ }
59
+
60
+ // Handle ZodDefault
61
+ else if (schema instanceof z.ZodDefault) {
62
+ const { defaultValue, innerType: innerSchema } = schema._def
63
+ result = transformSchemaKeysWithTransformer(
64
+ innerSchema,
65
+ transformer,
66
+ ).default(defaultValue())
67
+ }
68
+
69
+ // Handle ZodEffects (includes .refine(), .transform(), etc.)
70
+ else if (schema instanceof z.ZodEffects) {
71
+ const { schema: innerSchema } = schema._def
72
+ const transformedInner = transformSchemaKeysWithTransformer(
73
+ innerSchema,
74
+ transformer,
75
+ )
76
+
77
+ // We need to reconstruct the effects
78
+ // This is a simplified version - effects are not transformed, just passed through
79
+ result = transformedInner
80
+ }
81
+
82
+ // Handle ZodUnion
83
+ else if (schema instanceof z.ZodUnion) {
84
+ const { options } = schema._def
85
+ const transformedOptions = options.map((option: ZodTypeAny) =>
86
+ transformSchemaKeysWithTransformer(option, transformer),
87
+ )
88
+ result = z.union(
89
+ transformedOptions as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]],
90
+ )
91
+ }
92
+
93
+ // Handle ZodIntersection
94
+ else if (schema instanceof z.ZodIntersection) {
95
+ const { left, right } = schema._def
96
+ result = z.intersection(
97
+ transformSchemaKeysWithTransformer(left, transformer),
98
+ transformSchemaKeysWithTransformer(right, transformer),
99
+ )
100
+ }
101
+
102
+ // Propagate openapi metadata
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ const { openapi } = schema._def as any
105
+ if (openapi && result !== schema) {
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ ;(result._def as any).openapi = openapi
108
+ }
109
+
110
+ return result
111
+ }
112
+
113
+ /**
114
+ * Transforms all keys in a Zod schema from one case to another (e.g., snake_case to camelCase).
115
+ * This function recursively transforms all nested schemas as well.
116
+ *
117
+ * @param schema - The Zod schema to transform
118
+ * @param toCase - The target case (e.g., CaseTransformer.Camel)
119
+ * @returns A new Zod schema with transformed keys
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const snakeSchema = z.object({
124
+ * first_name: z.string(),
125
+ * last_name: z.string(),
126
+ * })
127
+ *
128
+ * const camelSchema = transformSchemaKeys(snakeSchema, CaseTransformer.Camel)
129
+ * // Results in: { firstName: z.string(), lastName: z.string() }
130
+ * ```
131
+ */
132
+ export function transformSchemaKeys<T extends ZodTypeAny>(
133
+ schema: T,
134
+ toCase: CaseTransformer,
135
+ ): T {
136
+ const transformer = CASE_TRANSFORMERS_MAPPING[toCase]
137
+
138
+ if (!transformer) {
139
+ throw new Error(`Unsupported case transformer: ${toCase}`)
140
+ }
141
+
142
+ return transformSchemaKeysWithTransformer(schema, transformer) as T
143
+ }