@reservamos/browser-analytics 0.1.4-alpha.9 → 0.2.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.
Files changed (38) hide show
  1. package/dist/browser-analytics.cjs +3 -3
  2. package/dist/browser-analytics.d.ts +429 -166
  3. package/dist/browser-analytics.esm.js +2822 -224
  4. package/dist/browser-analytics.iife.js +3 -3
  5. package/package.json +2 -1
  6. package/src/events/customEvent/customEventSchema.ts +19 -0
  7. package/src/events/customEvent/index.ts +7 -0
  8. package/src/events/customEvent/trackCustomEvent.ts +19 -0
  9. package/src/events/identify/identify.ts +17 -10
  10. package/src/events/identify/identifySchema.ts +2 -1
  11. package/src/events/identify/index.ts +3 -1
  12. package/src/events/interestInHome/trackInterestInHome.ts +7 -2
  13. package/src/events/interestInSearch/trackInterestInSearch.ts +7 -2
  14. package/src/events/passengersCreated/passengersCreatedSchema.ts +11 -48
  15. package/src/events/passengersCreated/trackPassengersCreated.ts +12 -3
  16. package/src/events/paymentAttempt/paymentAttemptSchema.ts +12 -45
  17. package/src/events/pickedDeparture/pickedDepartureSchema.ts +2 -9
  18. package/src/events/pickedDeparture/trackPickedDeparture.ts +7 -2
  19. package/src/events/purchaseAttempt/purchaseAttemptSchema.ts +6 -66
  20. package/src/events/purchaseAttempt/trackPurchaseAttempt.ts +8 -3
  21. package/src/events/search/trackSearch.ts +4 -2
  22. package/src/events/seatChange/trackSeatChange.ts +12 -3
  23. package/src/events/sharedSchemas/tripSchema.ts +42 -0
  24. package/src/events/viewResults/trackViewResults.ts +7 -2
  25. package/src/index.ts +17 -0
  26. package/src/init.ts +20 -3
  27. package/src/js-api-client.d.ts +15 -0
  28. package/src/profiles/createAnonymousProfile/createAnonymousProfile.test.ts +38 -0
  29. package/src/profiles/createAnonymousProfile/createAnonymousProfile.ts +78 -0
  30. package/src/profiles/createAnonymousProfile/createAnonymousProfileSchema.ts +15 -0
  31. package/src/profiles/createAnonymousProfile/index.ts +5 -0
  32. package/src/services/config.ts +24 -0
  33. package/src/services/fingerprint.ts +38 -2
  34. package/src/services/mixpanel.ts +21 -1
  35. package/src/services/validator.ts +21 -4
  36. package/src/track.ts +39 -5
  37. package/src/types/eventData.ts +5 -0
  38. package/src/util/primitiveFields.ts +70 -0
package/src/init.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { setConfig as setApiConfig } from '@reservamos/js-api-client';
1
2
  import { z } from 'zod';
3
+ import configService from '@/services/config';
2
4
  import fingerprintService from '@/services/fingerprint';
3
5
  import mixpanelService from '@/services/mixpanel';
4
6
  import validator, { InitConfigSchema } from './services/validator';
@@ -26,17 +28,32 @@ function onLoaded() {
26
28
  export async function init(config: InitConfig) {
27
29
  validator.parseInitProps(config);
28
30
 
29
- const { mixpanelToken, debug = false, identificationKey } = config;
31
+ const {
32
+ mixpanelToken,
33
+ debug = false,
34
+ identificationKey,
35
+ isSandbox = false,
36
+ mixpanelProxyUrl,
37
+ identifyProxyUrl,
38
+ } = config;
30
39
 
31
- await mixpanelService.init(mixpanelToken, debug);
40
+ await mixpanelService.init(mixpanelToken, debug, mixpanelProxyUrl);
32
41
 
33
42
  // Only mixpanel is required to be ready to dispatch the 'Tracker Ready' event
34
43
  try {
35
- await fingerprintService.initFingerprint(identificationKey);
44
+ await fingerprintService.initFingerprint(
45
+ identificationKey,
46
+ identifyProxyUrl,
47
+ );
36
48
  } catch (error) {
37
49
  console.error('Error initializing identification service:', error);
38
50
  }
39
51
 
52
+ const environment = isSandbox ? 'sandbox' : 'prod';
53
+ const apiConfig = configService.coreAPIConfig[environment];
54
+
55
+ setApiConfig(apiConfig);
56
+
40
57
  // Dispatch the 'Tracker Ready' event
41
58
  onLoaded();
42
59
  }
@@ -0,0 +1,15 @@
1
+ declare module '@reservamos/js-api-client' {
2
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
+ export const core: any; // Replace 'any' with a more specific type if possible
4
+
5
+ interface ConfigOptions {
6
+ coreUrl: string;
7
+ coreVersion?: string;
8
+ withCredentials?: boolean;
9
+ headers?: {
10
+ Origin?: string;
11
+ };
12
+ }
13
+
14
+ export function setConfig(options: ConfigOptions): void;
15
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tests for the createAnonymousProfile method.
3
+ * Cases:
4
+ * - Should not throw an error if the payload is invalid
5
+ * - Should not throw an error if the payload is valid but the API call fails.
6
+ */
7
+
8
+ import type { CreateAnonymousProfileProps } from './createAnonymousProfileSchema';
9
+ import { describe, expect, it } from 'vitest';
10
+ import validatorService from '@/services/validator';
11
+ import createAnonymousProfile from './createAnonymousProfile';
12
+ import CreateAnonymousProfileSchema from './createAnonymousProfileSchema';
13
+
14
+ describe('createAnonymousProfile', () => {
15
+ it('should not throw an error if the payload is invalid', async () => {
16
+ const invalidPayload = {};
17
+ expect(() =>
18
+ validatorService.validateProps(
19
+ invalidPayload,
20
+ CreateAnonymousProfileSchema,
21
+ ),
22
+ ).toThrow();
23
+ await expect(createAnonymousProfile(invalidPayload)).resolves.not.toThrow();
24
+ });
25
+
26
+ it('should not throw an error if the payload is valid but the API call fails', async () => {
27
+ const validPayload: CreateAnonymousProfileProps = {
28
+ email: 'test@test.com',
29
+ };
30
+ expect(() =>
31
+ validatorService.validateProps(
32
+ validPayload,
33
+ CreateAnonymousProfileSchema,
34
+ ),
35
+ ).not.toThrow();
36
+ await expect(createAnonymousProfile(validPayload)).resolves.not.toThrow();
37
+ });
38
+ });
@@ -0,0 +1,78 @@
1
+ import type { CreateAnonymousProfileProps } from './createAnonymousProfileSchema';
2
+ import { core as coreApi } from '@reservamos/js-api-client';
3
+ import fingerprintService from '@/services/fingerprint';
4
+ import mixpanelService from '@/services/mixpanel';
5
+ import validatorService from '@/services/validator';
6
+ import CreateAnonymousProfileSchema from './createAnonymousProfileSchema';
7
+
8
+ type IdentifierKey = 'phone' | 'email';
9
+
10
+ interface AnonymousProfilePayload {
11
+ identifier_key: IdentifierKey;
12
+ identifier_value: string;
13
+ details: Record<string, string | number | boolean>;
14
+ }
15
+
16
+ /**
17
+ * Transforms the identifier object based on the provided values.
18
+ * @param {CreateAnonymousProfileProps} values - The values to transform.
19
+ * @returns {AnonymousProfilePayload} The transformed identifier object.
20
+ */
21
+ function getAnonymousProfilePayload(
22
+ values: CreateAnonymousProfileProps,
23
+ ): AnonymousProfilePayload {
24
+ let identifier_key: IdentifierKey = 'phone';
25
+ let identifier_value: string = values.phone || '';
26
+ const details: Record<string, string | number | boolean> = {};
27
+
28
+ if (values.email) {
29
+ identifier_key = 'email';
30
+ identifier_value = values.email;
31
+ }
32
+
33
+ Object.entries(values).forEach(([key, value]) => {
34
+ if (key !== 'email' && key !== identifier_key && value !== undefined) {
35
+ details[key] = value as string | number | boolean;
36
+ }
37
+ });
38
+
39
+ return {
40
+ identifier_key,
41
+ identifier_value,
42
+ details,
43
+ };
44
+ }
45
+
46
+ interface AnonymousIdentifier {
47
+ key: string;
48
+ value: string;
49
+ }
50
+
51
+ /**
52
+ * Creates an anonymous profile using the provided payload.
53
+ * @param {CreateAnonymousProfileProps} payload - The payload to create the anonymous profile.
54
+ * @returns {Promise<void>} - A promise that resolves when the anonymous profile is created.
55
+ */
56
+ async function createAnonymousProfile(
57
+ payload: CreateAnonymousProfileProps,
58
+ ): Promise<void> {
59
+ try {
60
+ validatorService.validateProps(payload, CreateAnonymousProfileSchema);
61
+
62
+ const identifiers: AnonymousIdentifier[] = [];
63
+ const userFingerprintId = fingerprintService.getCachedFingerprint();
64
+ const distinctId = mixpanelService.getMixpanelDistinctId();
65
+
66
+ if (userFingerprintId)
67
+ identifiers.push({ key: 'fingerprint_id', value: userFingerprintId });
68
+ if (distinctId) identifiers.push({ key: 'distinct_id', value: distinctId });
69
+
70
+ const dataPayload = getAnonymousProfilePayload(payload);
71
+
72
+ await coreApi.createAnonymousProfile({ ...dataPayload, identifiers });
73
+ } catch (error) {
74
+ console.error('Could not create anonymous profile:', error);
75
+ }
76
+ }
77
+
78
+ export default createAnonymousProfile;
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ const CreateAnonymousProfileSchema = z
4
+ .object({
5
+ email: z.string().email().optional(),
6
+ phone: z.string().optional(),
7
+ })
8
+ .refine((data) => data.email || data.phone, {
9
+ message: "At least one of 'email' or 'phone' must be provided",
10
+ });
11
+
12
+ type CreateAnonymousProfileProps = z.infer<typeof CreateAnonymousProfileSchema>;
13
+
14
+ export type { CreateAnonymousProfileProps };
15
+ export default CreateAnonymousProfileSchema;
@@ -0,0 +1,5 @@
1
+ import type { CreateAnonymousProfileProps } from './createAnonymousProfileSchema';
2
+ import createAnonymousProfile from './createAnonymousProfile';
3
+
4
+ export type { CreateAnonymousProfileProps };
5
+ export default createAnonymousProfile;
@@ -0,0 +1,24 @@
1
+ const origin = window.location.origin;
2
+
3
+ const coreAPIConfig = {
4
+ sandbox: {
5
+ coreUrl: 'https://datalake-api-dev.reservamossaas.com/api',
6
+ coreVersion: 'v1',
7
+ headers: {
8
+ Origin: origin,
9
+ },
10
+ },
11
+ prod: {
12
+ coreUrl: 'https://datalake-api-dev.reservamossaas.com/api',
13
+ coreVersion: 'v1',
14
+ headers: {
15
+ Origin: origin,
16
+ },
17
+ },
18
+ };
19
+
20
+ const configService = {
21
+ coreAPIConfig,
22
+ };
23
+
24
+ export default configService;
@@ -1,4 +1,7 @@
1
- import { FpjsClient } from '@fingerprintjs/fingerprintjs-pro-spa';
1
+ import {
2
+ FingerprintJSPro,
3
+ FpjsClient,
4
+ } from '@fingerprintjs/fingerprintjs-pro-spa';
2
5
 
3
6
  declare global {
4
7
  interface Window {
@@ -6,12 +9,15 @@ declare global {
6
9
  fingerprintConfig: {
7
10
  cacheName?: string;
8
11
  cacheTimeInDays?: number;
12
+ proxyUrl?: string;
9
13
  };
10
14
  }
11
15
  }
12
16
 
13
17
  const DEFAULT_CACHE_NAME = 'default_fingerprint_cache';
14
18
  const DEFAULT_CACHE_TIME_IN_DAYS = 7;
19
+ const PROXY_QUERY_PARAM =
20
+ 'apiKey=<apiKey>&version=<version>&loaderVersion=<loaderVersion>';
15
21
 
16
22
  /**
17
23
  * Calculates the expiration date based on the cache time.
@@ -62,17 +68,46 @@ const setCachedFingerprint = (fingerprint: string): void => {
62
68
  localStorage.setItem(cacheName, JSON.stringify(cacheData));
63
69
  };
64
70
 
71
+ /**
72
+ * Builds a proxy URL by appending a query parameter to the given URL.
73
+ *
74
+ * This function takes a base URL and appends a predefined query parameter
75
+ * to create a proxy URL. The query parameter is defined by the constant
76
+ * PROXY_QUERY_PARAM, which should be declared elsewhere in the file.
77
+ * @param {string} url - The base URL to which the proxy query parameter will be appended.
78
+ * @returns {string} The constructed proxy URL with the appended query parameter.
79
+ */
80
+ function buildProxyUrl(url: string) {
81
+ return `${url}?${PROXY_QUERY_PARAM}`;
82
+ }
83
+
65
84
  /**
66
85
  * Initializes identification service with the provided API key and stores the instance in the window object.
67
86
  * @param {string} apiKey - The API key for Fingerprint.js.
87
+ * @param {string} proxyUrl - Optional proxy URL for Fingerprint.js requests.
68
88
  * @returns {Promise<void>} A promise that resolves when initialization is complete.
69
89
  * @throws {Error} Throws an error if initialization fails.
70
90
  */
71
- async function initFingerprint(apiKey: string): Promise<void> {
91
+ async function initFingerprint(
92
+ apiKey: string,
93
+ proxyUrl?: string,
94
+ ): Promise<void> {
95
+ const customEndpointArray = [FingerprintJSPro.defaultEndpoint];
96
+
97
+ const customScriptEndpointArray = [FingerprintJSPro.defaultScriptUrlPattern];
98
+
99
+ if (proxyUrl) {
100
+ // @ts-expect-error - This is a valid operation but fingerprintjs-pro-spa types are incorrect
101
+ customEndpointArray.unshift(proxyUrl);
102
+ customScriptEndpointArray.unshift(buildProxyUrl(proxyUrl));
103
+ }
104
+
72
105
  window.fingerprintConfig = {};
73
106
  window.fpClient = new FpjsClient({
74
107
  loadOptions: {
75
108
  apiKey,
109
+ endpoint: customEndpointArray,
110
+ scriptUrlPattern: customScriptEndpointArray,
76
111
  },
77
112
  });
78
113
 
@@ -134,6 +169,7 @@ const fingerprintService = {
134
169
  initFingerprint,
135
170
  getFingerprint,
136
171
  isFingerprintReady,
172
+ getCachedFingerprint,
137
173
  };
138
174
 
139
175
  export default fingerprintService;
@@ -10,9 +10,14 @@ declare global {
10
10
  * Initializes Mixpanel with the provided token and debug flag.
11
11
  * @param {string} mixpanelToken - The Mixpanel token used for authenticating API requests.
12
12
  * @param {boolean} debug - Optional flag to enable or disable debug mode.
13
+ * @param {string} proxyUrl - Optional proxy URL for Mixpanel requests.
13
14
  * @returns {Promise<void>} A promise that resolves when Mixpanel is initialized.
14
15
  */
15
- function init(mixpanelToken: string, debug = false): Promise<void> {
16
+ function init(
17
+ mixpanelToken: string,
18
+ debug = false,
19
+ proxyUrl?: string,
20
+ ): Promise<void> {
16
21
  return new Promise<void>((resolve) => {
17
22
  mixpanel.init(mixpanelToken, {
18
23
  debug,
@@ -20,6 +25,7 @@ function init(mixpanelToken: string, debug = false): Promise<void> {
20
25
  window.mixpanel = mixpanel;
21
26
  resolve();
22
27
  },
28
+ ...(proxyUrl && { api_host: proxyUrl }),
23
29
  });
24
30
  });
25
31
  }
@@ -78,12 +84,26 @@ function track(eventName: string, properties: Record<string, unknown>): void {
78
84
  mixpanel.track(eventName, properties);
79
85
  }
80
86
 
87
+ /**
88
+ * Retrieves the distinct ID from Mixpanel.
89
+ * This function checks if Mixpanel is initialized and returns the current user's
90
+ * distinct ID. If Mixpanel is not initialized, it returns null.
91
+ * @returns {string | null} The distinct ID if available, or null if Mixpanel is not initialized.
92
+ */
93
+ export const getMixpanelDistinctId = (): string | null => {
94
+ if (mixpanel && mixpanel.get_distinct_id) {
95
+ return mixpanel.get_distinct_id();
96
+ }
97
+ return null;
98
+ };
99
+
81
100
  const mixpanelService = {
82
101
  init,
83
102
  isReady,
84
103
  track,
85
104
  identify,
86
105
  attachProperty,
106
+ getMixpanelDistinctId,
87
107
  };
88
108
 
89
109
  export default mixpanelService;
@@ -1,4 +1,5 @@
1
1
  import { z, ZodError, ZodSchema } from 'zod';
2
+ import { customEventSchema } from '@/events/customEvent/customEventSchema';
2
3
  import IdentifySchema from '@/events/identify/identifySchema';
3
4
  import { interestInHomeSchema } from '@/events/interestInHome';
4
5
  import { interestInSearchSchema } from '@/events/interestInSearch';
@@ -25,6 +26,19 @@ export const InitConfigSchema = z.object({
25
26
  * Key for tracking user identities.
26
27
  */
27
28
  identificationKey: z.string().min(1, 'Identification key is required'),
29
+ /**
30
+ * Optional flag to determine if the sandbox environment should be used.
31
+ * When set to true, the sandbox environment will be used; otherwise, the production environment will be used.
32
+ */
33
+ isSandbox: z.boolean().optional().default(false),
34
+ /**
35
+ * Optional proxy URL for Mixpanel requests.
36
+ */
37
+ mixpanelProxyUrl: z.string().optional(),
38
+ /**
39
+ * Optional proxy URL for identify requests.
40
+ */
41
+ identifyProxyUrl: z.string().optional(),
28
42
  });
29
43
  interface CustomError {
30
44
  field: string;
@@ -82,16 +96,18 @@ const eventSchemas: Record<string, z.ZodSchema> = {
82
96
  'Picked Departure': pickedDepartureSchema,
83
97
  };
84
98
 
85
- type EventData = z.infer<(typeof eventSchemas)[keyof typeof eventSchemas]>;
99
+ type EventDataSchema = z.infer<
100
+ (typeof eventSchemas)[keyof typeof eventSchemas]
101
+ >;
86
102
 
87
103
  /**
88
104
  * Validates the event data against the predefined schema for the given event name.
89
105
  * @param {string} eventName - The name of the event to validate.
90
- * @param {EventData} eventData - The data of the event to validate.
106
+ * @param {EventDataSchema} eventData - The data of the event to validate.
91
107
  * @throws {Error} - Throws an error if validation fails.
92
108
  */
93
- function parseEventProps(eventName: string, eventData: EventData): void {
94
- const eventSchema = eventSchemas[eventName];
109
+ function parseEventProps(eventName: string, eventData: EventDataSchema): void {
110
+ const eventSchema = eventSchemas[eventName] || customEventSchema;
95
111
 
96
112
  if (!eventSchema) {
97
113
  throw { message: `Event ${eventName} not found` };
@@ -148,6 +164,7 @@ const validatorService = {
148
164
  parseEventProps,
149
165
  parseInitProps,
150
166
  parseIdentifyProps,
167
+ validateProps,
151
168
  };
152
169
 
153
170
  // Export the validator object
package/src/track.ts CHANGED
@@ -1,22 +1,55 @@
1
1
  import fingerprintService from '@/services/fingerprint';
2
2
  import mixpanelService from '@/services/mixpanel';
3
3
  import validator from '@/services/validator';
4
+ import EventData, { AllowedPrimitive } from './types/eventData';
4
5
 
5
6
  /**
6
- * List of events that trigger the fingerprint to be sent with the event. other eventes will only fetch the cached fingerprint.
7
+ * List of events that trigger the fingerprint to be sent with the event. other events will only fetch the cached fingerprint.
7
8
  */
8
- const FP_TRIGGER_EVENTS = ['Track Test', 'Search', 'View Results'];
9
+ const FP_TRIGGER_EVENTS = ['Search', 'View Results'];
10
+
11
+ /**
12
+ * Simplifies the structure of event data by flattening nested objects and arrays.
13
+ * @param {object} data - The event data to simplify.
14
+ * @returns {EventData} - The flattened event data.
15
+ */
16
+ function flattenEventData(data: object): EventData {
17
+ return Object.entries(data).reduce((flattenedData, [key, value]) => {
18
+ if (!Array.isArray(value)) {
19
+ flattenedData[key] = value as AllowedPrimitive;
20
+ return flattenedData;
21
+ }
22
+
23
+ value.forEach((item) => {
24
+ if (typeof item !== 'object' || item === null) return;
25
+
26
+ Object.entries(item).forEach(([innerKey, innerValue]) => {
27
+ if (!flattenedData[innerKey]) {
28
+ flattenedData[innerKey] = [innerValue as AllowedPrimitive];
29
+ } else {
30
+ (flattenedData[innerKey] as AllowedPrimitive[]).push(
31
+ innerValue as AllowedPrimitive,
32
+ );
33
+ }
34
+ });
35
+ });
36
+
37
+ return flattenedData;
38
+ }, {} as EventData);
39
+ }
9
40
 
10
41
  /**
11
42
  * Base function to track events with Mixpanel.
12
43
  * This function adds default properties like User Fingerprint.
13
44
  * @param {string} eventName - The name of the event to track.
14
45
  * @param {object} eventProperties - The properties of the event to track.
46
+ * @param {EventData} meta - Additional metadata to include in the event.
15
47
  * @throws {Error} Throws an error if Mixpanel or Fingerprint is not ready.
16
48
  */
17
49
  export async function trackEvent(
18
50
  eventName: string,
19
51
  eventProperties: object,
52
+ meta: EventData = {},
20
53
  ): Promise<void> {
21
54
  if (!mixpanelService.isReady()) {
22
55
  throw new Error('Mixpanel is not initialized.');
@@ -31,14 +64,15 @@ export async function trackEvent(
31
64
  'User Fingerprint': fingerprint,
32
65
  };
33
66
 
67
+ const simplifiedData = flattenEventData(eventProperties);
34
68
  const properties = {
35
69
  ...defaultProperties,
36
- ...eventProperties,
70
+ ...simplifiedData,
71
+ ...meta,
37
72
  };
38
73
 
39
74
  mixpanelService.track(eventName, properties);
40
75
  } catch (error) {
41
- console.error('Error tracking event:', error);
42
- throw error;
76
+ console.error(`Error tracking event '${eventName}':`, error);
43
77
  }
44
78
  }
@@ -0,0 +1,5 @@
1
+ export type AllowedPrimitive = string | boolean | number | undefined;
2
+
3
+ type EventData = Record<string, AllowedPrimitive | AllowedPrimitive[]>;
4
+
5
+ export default EventData;
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ import dateValidation from '@/util/dateValidation';
3
+
4
+ /**
5
+ * Creates a Zod schema for a string field with a custom error message.
6
+ * @param {string} fieldName - The name of the field to be validated.
7
+ * @returns {z.ZodEffects<z.ZodString, string, string>} A Zod schema for string validation.
8
+ */
9
+ export const stringField = (fieldName: string) =>
10
+ z.string().refine((val) => val, {
11
+ message: `${fieldName} must be a string`,
12
+ });
13
+
14
+ /**
15
+ * Creates a Zod schema for a number field with a custom error message.
16
+ * @param {string} fieldName - The name of the field to be validated.
17
+ * @returns {z.ZodEffects<z.ZodNumber, number, number>} A Zod schema for number validation.
18
+ */
19
+ export const numberField = (fieldName: string) =>
20
+ z.number().refine((val) => val, {
21
+ message: `${fieldName} must be a number`,
22
+ });
23
+
24
+ /**
25
+ * Creates a Zod schema for an integer field with a custom error message.
26
+ * @param {string} fieldName - The name of the field to be validated.
27
+ * @returns {z.ZodEffects<z.ZodNumber, number, number>} A Zod schema for integer validation.
28
+ */
29
+ export const intField = (fieldName: string) =>
30
+ z
31
+ .number()
32
+ .int()
33
+ .refine((val) => val, {
34
+ message: `${fieldName} must be an integer`,
35
+ });
36
+
37
+ /**
38
+ * Creates a Zod schema for a date field with custom validation and error message.
39
+ * @param {string} fieldName - The name of the field to be validated.
40
+ * @returns {z.ZodEffects<z.ZodString, string, string>} A Zod schema for date validation.
41
+ */
42
+ export const dateField = (fieldName: string) =>
43
+ z.string().refine(dateValidation, {
44
+ message: `Invalid ${fieldName} datetime format`,
45
+ });
46
+
47
+ /**
48
+ * Creates a Zod schema for an array field with a custom schema and optional minimum length.
49
+ * @param {z.ZodType<T>} schema - The Zod schema to be applied to each array element.
50
+ * @param {string} fieldName - The name of the field to be validated.
51
+ * @param {number} [minLength] - The optional minimum length of the array.
52
+ * @returns {z.ZodArray<z.ZodType<T>>} A Zod schema for array validation.
53
+ * @template T
54
+ */
55
+ export const arrayField = <T>(
56
+ schema: z.ZodType<T>,
57
+ fieldName: string,
58
+ minLength?: number,
59
+ ) => {
60
+ const arraySchema = z.array(schema);
61
+
62
+ if (minLength !== undefined) {
63
+ return arraySchema.min(
64
+ minLength,
65
+ `${fieldName} must have at least ${minLength} items`,
66
+ );
67
+ }
68
+
69
+ return arraySchema;
70
+ };