@htlkg/data 0.0.24 → 0.0.25
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/README.md +7 -0
- package/dist/hooks/index.d.ts +22 -2
- package/dist/hooks/index.js +52 -8
- package/dist/hooks/index.js.map +1 -1
- package/dist/index-ZmmFz38a.d.ts +1089 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +372 -53
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.d.ts +4 -714
- package/dist/mutations/index.js +163 -47
- package/dist/mutations/index.js.map +1 -1
- package/dist/queries/index.d.ts +48 -2
- package/dist/queries/index.js +43 -0
- package/dist/queries/index.js.map +1 -1
- package/package.json +3 -3
- package/src/hooks/activityLogs/index.ts +5 -0
- package/src/hooks/activityLogs/useActivityLogs.ts +63 -0
- package/src/hooks/brands/usePaginatedBrands.ts +9 -1
- package/src/hooks/index.ts +9 -0
- package/src/index.ts +3 -0
- package/src/mutations/contacts.ts +12 -75
- package/src/mutations/index.ts +1 -0
- package/src/queries/activityLogs.ts +87 -0
- package/src/queries/index.ts +8 -0
- package/src/validation/contact.schemas.test.ts +975 -0
- package/src/validation/contact.schemas.ts +611 -0
- package/src/validation/index.ts +54 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Validation Schemas
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive Zod validation schemas for Contact entities.
|
|
5
|
+
* Includes field-level validators, custom field schemas, and complete entity schemas.
|
|
6
|
+
*
|
|
7
|
+
* @module @htlkg/data/validation/contact.schemas
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
// ============================================
|
|
13
|
+
// Field-Level Validators
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Email validation schema
|
|
18
|
+
* - Must be valid email format
|
|
19
|
+
* - Transforms to lowercase for consistency
|
|
20
|
+
* - Max 254 characters (RFC 5321)
|
|
21
|
+
*/
|
|
22
|
+
export const emailSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.email("Invalid email address")
|
|
27
|
+
.max(254, "Email must not exceed 254 characters");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional email validation schema
|
|
31
|
+
* Same rules as emailSchema but allows undefined
|
|
32
|
+
*/
|
|
33
|
+
export const optionalEmailSchema = z
|
|
34
|
+
.string()
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.email("Invalid email address")
|
|
38
|
+
.max(254, "Email must not exceed 254 characters")
|
|
39
|
+
.optional();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Phone number validation schema
|
|
43
|
+
* Supports international formats:
|
|
44
|
+
* - E.164 format: +14155552671
|
|
45
|
+
* - With spaces/dashes: +1 415-555-2671
|
|
46
|
+
* - Local formats: (415) 555-2671
|
|
47
|
+
* - Minimum 7 digits, maximum 15 digits (ITU-T E.164)
|
|
48
|
+
*/
|
|
49
|
+
const PHONE_REGEX = /^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,3}[)]?[-\s\.]?[0-9]{1,4}[-\s\.]?[0-9]{1,4}[-\s\.]?[0-9]{0,9}$/;
|
|
50
|
+
|
|
51
|
+
export const phoneSchema = z
|
|
52
|
+
.string()
|
|
53
|
+
.trim()
|
|
54
|
+
.min(7, "Phone number must be at least 7 characters")
|
|
55
|
+
.max(20, "Phone number must not exceed 20 characters")
|
|
56
|
+
.regex(PHONE_REGEX, "Invalid phone number format");
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Optional phone validation schema
|
|
60
|
+
* Validates format only if value is provided and non-empty
|
|
61
|
+
*/
|
|
62
|
+
export const optionalPhoneSchema = z
|
|
63
|
+
.string()
|
|
64
|
+
.trim()
|
|
65
|
+
.optional()
|
|
66
|
+
.refine(
|
|
67
|
+
(val) => {
|
|
68
|
+
if (!val || val === "") return true;
|
|
69
|
+
return PHONE_REGEX.test(val) && val.length >= 7 && val.length <= 20;
|
|
70
|
+
},
|
|
71
|
+
{ message: "Invalid phone number format" }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* First name validation schema
|
|
76
|
+
* - Required, 1-100 characters
|
|
77
|
+
* - Trims whitespace
|
|
78
|
+
* - Allows Unicode letters, spaces, hyphens, apostrophes
|
|
79
|
+
*/
|
|
80
|
+
const NAME_REGEX = /^[\p{L}\s\-'\.]+$/u;
|
|
81
|
+
|
|
82
|
+
export const firstNameSchema = z
|
|
83
|
+
.string()
|
|
84
|
+
.trim()
|
|
85
|
+
.min(1, "First name is required")
|
|
86
|
+
.max(100, "First name must not exceed 100 characters")
|
|
87
|
+
.regex(NAME_REGEX, "First name contains invalid characters");
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Last name validation schema
|
|
91
|
+
* - Required, 1-100 characters
|
|
92
|
+
* - Trims whitespace
|
|
93
|
+
* - Allows Unicode letters, spaces, hyphens, apostrophes
|
|
94
|
+
*/
|
|
95
|
+
export const lastNameSchema = z
|
|
96
|
+
.string()
|
|
97
|
+
.trim()
|
|
98
|
+
.min(1, "Last name is required")
|
|
99
|
+
.max(100, "Last name must not exceed 100 characters")
|
|
100
|
+
.regex(NAME_REGEX, "Last name contains invalid characters");
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Optional first name validation schema
|
|
104
|
+
*/
|
|
105
|
+
export const optionalFirstNameSchema = z
|
|
106
|
+
.string()
|
|
107
|
+
.trim()
|
|
108
|
+
.min(1, "First name cannot be empty if provided")
|
|
109
|
+
.max(100, "First name must not exceed 100 characters")
|
|
110
|
+
.regex(NAME_REGEX, "First name contains invalid characters")
|
|
111
|
+
.optional();
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Optional last name validation schema
|
|
115
|
+
*/
|
|
116
|
+
export const optionalLastNameSchema = z
|
|
117
|
+
.string()
|
|
118
|
+
.trim()
|
|
119
|
+
.min(1, "Last name cannot be empty if provided")
|
|
120
|
+
.max(100, "Last name must not exceed 100 characters")
|
|
121
|
+
.regex(NAME_REGEX, "Last name contains invalid characters")
|
|
122
|
+
.optional();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Locale validation schema (BCP 47 language tag)
|
|
126
|
+
* Examples: "en", "en-US", "pt-BR", "zh-Hans-CN"
|
|
127
|
+
*/
|
|
128
|
+
const LOCALE_REGEX = /^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$/;
|
|
129
|
+
|
|
130
|
+
export const localeSchema = z
|
|
131
|
+
.string()
|
|
132
|
+
.regex(LOCALE_REGEX, "Invalid locale format. Use BCP 47 format (e.g., 'en', 'en-US', 'pt-BR')");
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Optional locale validation schema
|
|
136
|
+
*/
|
|
137
|
+
export const optionalLocaleSchema = z
|
|
138
|
+
.string()
|
|
139
|
+
.regex(LOCALE_REGEX, "Invalid locale format. Use BCP 47 format (e.g., 'en', 'en-US', 'pt-BR')")
|
|
140
|
+
.optional();
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* ISO 8601 date string regex pattern
|
|
144
|
+
* Matches: YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS, YYYY-MM-DDTHH:MM:SSZ, YYYY-MM-DDTHH:MM:SS±HH:MM
|
|
145
|
+
*/
|
|
146
|
+
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* ISO 8601 date string validation schema
|
|
150
|
+
* Validates date strings in ISO 8601 format
|
|
151
|
+
*/
|
|
152
|
+
export const isoDateStringSchema = z
|
|
153
|
+
.string()
|
|
154
|
+
.refine(
|
|
155
|
+
(val) => {
|
|
156
|
+
// First check format matches ISO 8601
|
|
157
|
+
if (!ISO_DATE_REGEX.test(val)) return false;
|
|
158
|
+
// Then verify it's a valid date
|
|
159
|
+
const date = new Date(val);
|
|
160
|
+
return !isNaN(date.getTime());
|
|
161
|
+
},
|
|
162
|
+
{ message: "Invalid date format. Use ISO 8601 format (e.g., '2024-01-15T10:30:00Z')" }
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Optional ISO 8601 date string validation schema
|
|
167
|
+
*/
|
|
168
|
+
export const optionalIsoDateStringSchema = z
|
|
169
|
+
.string()
|
|
170
|
+
.refine(
|
|
171
|
+
(val) => {
|
|
172
|
+
// First check format matches ISO 8601
|
|
173
|
+
if (!ISO_DATE_REGEX.test(val)) return false;
|
|
174
|
+
// Then verify it's a valid date
|
|
175
|
+
const date = new Date(val);
|
|
176
|
+
return !isNaN(date.getTime());
|
|
177
|
+
},
|
|
178
|
+
{ message: "Invalid date format. Use ISO 8601 format (e.g., '2024-01-15T10:30:00Z')" }
|
|
179
|
+
)
|
|
180
|
+
.optional();
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* UUID validation schema
|
|
184
|
+
*/
|
|
185
|
+
export const uuidSchema = z
|
|
186
|
+
.string()
|
|
187
|
+
.uuid("Invalid UUID format")
|
|
188
|
+
.or(z.string().min(1, "ID is required"));
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Brand ID validation schema
|
|
192
|
+
*/
|
|
193
|
+
export const brandIdSchema = z
|
|
194
|
+
.string()
|
|
195
|
+
.min(1, "Brand ID is required");
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Tag validation schema
|
|
199
|
+
* - Non-empty string
|
|
200
|
+
* - Max 50 characters per tag
|
|
201
|
+
* - Trimmed and lowercased for consistency
|
|
202
|
+
*/
|
|
203
|
+
export const tagSchema = z
|
|
204
|
+
.string()
|
|
205
|
+
.trim()
|
|
206
|
+
.toLowerCase()
|
|
207
|
+
.min(1, "Tag cannot be empty")
|
|
208
|
+
.max(50, "Tag must not exceed 50 characters");
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Tags array validation schema
|
|
212
|
+
* - Array of valid tags
|
|
213
|
+
* - Max 100 tags per contact
|
|
214
|
+
*/
|
|
215
|
+
export const tagsArraySchema = z
|
|
216
|
+
.array(tagSchema)
|
|
217
|
+
.max(100, "Cannot have more than 100 tags")
|
|
218
|
+
.optional();
|
|
219
|
+
|
|
220
|
+
// ============================================
|
|
221
|
+
// Custom Field Validation Schemas
|
|
222
|
+
// ============================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Supported custom field types
|
|
226
|
+
*/
|
|
227
|
+
export const CustomFieldType = {
|
|
228
|
+
STRING: "string",
|
|
229
|
+
NUMBER: "number",
|
|
230
|
+
BOOLEAN: "boolean",
|
|
231
|
+
DATE: "date",
|
|
232
|
+
ARRAY: "array",
|
|
233
|
+
OBJECT: "object",
|
|
234
|
+
} as const;
|
|
235
|
+
|
|
236
|
+
export type CustomFieldTypeValue = (typeof CustomFieldType)[keyof typeof CustomFieldType];
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Custom field definition schema
|
|
240
|
+
* Defines the structure for a single custom field
|
|
241
|
+
*/
|
|
242
|
+
export const customFieldDefinitionSchema = z.object({
|
|
243
|
+
type: z.enum(["string", "number", "boolean", "date", "array", "object"]),
|
|
244
|
+
required: z.boolean().optional().default(false),
|
|
245
|
+
maxLength: z.number().positive().optional(),
|
|
246
|
+
minLength: z.number().nonnegative().optional(),
|
|
247
|
+
min: z.number().optional(),
|
|
248
|
+
max: z.number().optional(),
|
|
249
|
+
pattern: z.string().optional(),
|
|
250
|
+
enum: z.array(z.union([z.string(), z.number()])).optional(),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
export type CustomFieldDefinition = z.infer<typeof customFieldDefinitionSchema>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Custom field value schema
|
|
257
|
+
* Validates a value based on its field definition
|
|
258
|
+
*/
|
|
259
|
+
export function createCustomFieldValueSchema(definition: CustomFieldDefinition): z.ZodTypeAny {
|
|
260
|
+
let schema: z.ZodTypeAny;
|
|
261
|
+
|
|
262
|
+
switch (definition.type) {
|
|
263
|
+
case "string":
|
|
264
|
+
schema = z.string();
|
|
265
|
+
if (definition.maxLength) {
|
|
266
|
+
schema = (schema as z.ZodString).max(definition.maxLength);
|
|
267
|
+
}
|
|
268
|
+
if (definition.minLength) {
|
|
269
|
+
schema = (schema as z.ZodString).min(definition.minLength);
|
|
270
|
+
}
|
|
271
|
+
if (definition.pattern) {
|
|
272
|
+
schema = (schema as z.ZodString).regex(new RegExp(definition.pattern));
|
|
273
|
+
}
|
|
274
|
+
if (definition.enum) {
|
|
275
|
+
schema = z.enum(definition.enum as [string, ...string[]]);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case "number":
|
|
280
|
+
schema = z.number();
|
|
281
|
+
if (definition.min !== undefined) {
|
|
282
|
+
schema = (schema as z.ZodNumber).min(definition.min);
|
|
283
|
+
}
|
|
284
|
+
if (definition.max !== undefined) {
|
|
285
|
+
schema = (schema as z.ZodNumber).max(definition.max);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case "boolean":
|
|
290
|
+
schema = z.boolean();
|
|
291
|
+
break;
|
|
292
|
+
|
|
293
|
+
case "date":
|
|
294
|
+
schema = isoDateStringSchema;
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case "array":
|
|
298
|
+
schema = z.array(z.any());
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case "object":
|
|
302
|
+
schema = z.record(z.any());
|
|
303
|
+
break;
|
|
304
|
+
|
|
305
|
+
default:
|
|
306
|
+
schema = z.any();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!definition.required) {
|
|
310
|
+
schema = schema.optional();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return schema;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Contact preferences schema
|
|
318
|
+
* Validates the preferences Record with type-safe structure
|
|
319
|
+
*
|
|
320
|
+
* Known preference fields with validation:
|
|
321
|
+
* - theme: "light" | "dark" | "system"
|
|
322
|
+
* - language: BCP 47 locale
|
|
323
|
+
* - notifications: { email: boolean, sms: boolean, push: boolean }
|
|
324
|
+
* - communication: { preferredChannel: "email" | "phone" | "sms" }
|
|
325
|
+
* - Other fields: any valid JSON value
|
|
326
|
+
*/
|
|
327
|
+
export const contactPreferencesSchema = z
|
|
328
|
+
.record(
|
|
329
|
+
z.string(),
|
|
330
|
+
z.union([
|
|
331
|
+
z.string(),
|
|
332
|
+
z.number(),
|
|
333
|
+
z.boolean(),
|
|
334
|
+
z.array(z.any()),
|
|
335
|
+
z.record(z.any()),
|
|
336
|
+
z.null(),
|
|
337
|
+
])
|
|
338
|
+
)
|
|
339
|
+
.optional()
|
|
340
|
+
.refine(
|
|
341
|
+
(preferences) => {
|
|
342
|
+
if (!preferences) return true;
|
|
343
|
+
|
|
344
|
+
// Validate known preference fields if present
|
|
345
|
+
if (preferences.theme && !["light", "dark", "system"].includes(preferences.theme as string)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (preferences.language && typeof preferences.language === "string") {
|
|
350
|
+
const localeResult = localeSchema.safeParse(preferences.language);
|
|
351
|
+
if (!localeResult.success) return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return true;
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
message: "Invalid preferences structure. Check theme (light/dark/system) and language (BCP 47) values.",
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Notification preferences sub-schema
|
|
363
|
+
*/
|
|
364
|
+
export const notificationPreferencesSchema = z.object({
|
|
365
|
+
email: z.boolean().optional().default(true),
|
|
366
|
+
sms: z.boolean().optional().default(false),
|
|
367
|
+
push: z.boolean().optional().default(false),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Communication preferences sub-schema
|
|
372
|
+
*/
|
|
373
|
+
export const communicationPreferencesSchema = z.object({
|
|
374
|
+
preferredChannel: z.enum(["email", "phone", "sms"]).optional().default("email"),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ============================================
|
|
378
|
+
// Complete Contact Schemas
|
|
379
|
+
// ============================================
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Base contact fields schema (shared between create and update)
|
|
383
|
+
*/
|
|
384
|
+
const baseContactFieldsSchema = {
|
|
385
|
+
brandId: brandIdSchema,
|
|
386
|
+
email: emailSchema,
|
|
387
|
+
phone: optionalPhoneSchema,
|
|
388
|
+
firstName: firstNameSchema,
|
|
389
|
+
lastName: lastNameSchema,
|
|
390
|
+
locale: optionalLocaleSchema,
|
|
391
|
+
gdprConsent: z.boolean({
|
|
392
|
+
required_error: "GDPR consent is required",
|
|
393
|
+
invalid_type_error: "GDPR consent must be a boolean",
|
|
394
|
+
}),
|
|
395
|
+
gdprConsentDate: optionalIsoDateStringSchema,
|
|
396
|
+
marketingOptIn: z.boolean().optional(),
|
|
397
|
+
preferences: contactPreferencesSchema,
|
|
398
|
+
tags: tagsArraySchema,
|
|
399
|
+
totalVisits: z.number().int().min(0, "Total visits cannot be negative").optional(),
|
|
400
|
+
lastVisitDate: optionalIsoDateStringSchema,
|
|
401
|
+
firstVisitDate: optionalIsoDateStringSchema,
|
|
402
|
+
legacyId: z.string().max(255, "Legacy ID must not exceed 255 characters").optional(),
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Audit fields for creation
|
|
407
|
+
*/
|
|
408
|
+
const createAuditFieldsSchema = {
|
|
409
|
+
createdAt: optionalIsoDateStringSchema,
|
|
410
|
+
createdBy: z.string().optional(),
|
|
411
|
+
updatedAt: optionalIsoDateStringSchema,
|
|
412
|
+
updatedBy: z.string().optional(),
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Audit fields for updates (includes soft delete)
|
|
417
|
+
*/
|
|
418
|
+
const updateAuditFieldsSchema = {
|
|
419
|
+
updatedAt: optionalIsoDateStringSchema,
|
|
420
|
+
updatedBy: z.string().optional(),
|
|
421
|
+
deletedAt: z.string().nullable().optional(),
|
|
422
|
+
deletedBy: z.string().nullable().optional(),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Create Contact Schema
|
|
427
|
+
* Full validation for creating a new contact
|
|
428
|
+
*/
|
|
429
|
+
export const createContactSchema = z.object({
|
|
430
|
+
...baseContactFieldsSchema,
|
|
431
|
+
...createAuditFieldsSchema,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Update Contact Schema
|
|
436
|
+
* Partial validation for updating an existing contact
|
|
437
|
+
* All fields except 'id' are optional
|
|
438
|
+
*/
|
|
439
|
+
export const updateContactSchema = z.object({
|
|
440
|
+
id: z.string().min(1, "Contact ID is required"),
|
|
441
|
+
brandId: brandIdSchema.optional(),
|
|
442
|
+
email: optionalEmailSchema,
|
|
443
|
+
phone: optionalPhoneSchema,
|
|
444
|
+
firstName: optionalFirstNameSchema,
|
|
445
|
+
lastName: optionalLastNameSchema,
|
|
446
|
+
locale: optionalLocaleSchema,
|
|
447
|
+
gdprConsent: z.boolean().optional(),
|
|
448
|
+
gdprConsentDate: optionalIsoDateStringSchema,
|
|
449
|
+
marketingOptIn: z.boolean().optional(),
|
|
450
|
+
preferences: contactPreferencesSchema,
|
|
451
|
+
tags: tagsArraySchema,
|
|
452
|
+
totalVisits: z.number().int().min(0, "Total visits cannot be negative").optional(),
|
|
453
|
+
lastVisitDate: optionalIsoDateStringSchema,
|
|
454
|
+
firstVisitDate: optionalIsoDateStringSchema,
|
|
455
|
+
legacyId: z.string().max(255, "Legacy ID must not exceed 255 characters").optional(),
|
|
456
|
+
...updateAuditFieldsSchema,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Merge Contacts Schema
|
|
461
|
+
* Validation for merging duplicate contacts
|
|
462
|
+
*/
|
|
463
|
+
export const mergeContactsSchema = z.object({
|
|
464
|
+
primaryId: z.string().min(1, "Primary contact ID is required"),
|
|
465
|
+
duplicateIds: z
|
|
466
|
+
.array(z.string().min(1, "Duplicate ID cannot be empty"))
|
|
467
|
+
.min(1, "At least one duplicate ID is required")
|
|
468
|
+
.max(50, "Cannot merge more than 50 contacts at once"),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Search/Filter Contact Schema
|
|
473
|
+
* Validation for contact search and filter operations
|
|
474
|
+
*/
|
|
475
|
+
export const searchContactSchema = z.object({
|
|
476
|
+
brandId: brandIdSchema.optional(),
|
|
477
|
+
email: z.string().optional(),
|
|
478
|
+
phone: z.string().optional(),
|
|
479
|
+
firstName: z.string().optional(),
|
|
480
|
+
lastName: z.string().optional(),
|
|
481
|
+
search: z.string().max(255, "Search query must not exceed 255 characters").optional(),
|
|
482
|
+
tags: z.array(z.string()).optional(),
|
|
483
|
+
gdprConsent: z.boolean().optional(),
|
|
484
|
+
marketingOptIn: z.boolean().optional(),
|
|
485
|
+
includeDeleted: z.boolean().optional().default(false),
|
|
486
|
+
limit: z.number().int().min(1).max(250).optional().default(25),
|
|
487
|
+
nextToken: z.string().optional(),
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Bulk Import Contact Schema
|
|
492
|
+
* Validation for importing contacts in bulk
|
|
493
|
+
*/
|
|
494
|
+
export const bulkImportContactSchema = z.object({
|
|
495
|
+
contacts: z
|
|
496
|
+
.array(createContactSchema.omit({ createdAt: true, createdBy: true, updatedAt: true, updatedBy: true }))
|
|
497
|
+
.min(1, "At least one contact is required")
|
|
498
|
+
.max(1000, "Cannot import more than 1000 contacts at once"),
|
|
499
|
+
skipDuplicates: z.boolean().optional().default(true),
|
|
500
|
+
updateExisting: z.boolean().optional().default(false),
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ============================================
|
|
504
|
+
// Type Exports
|
|
505
|
+
// ============================================
|
|
506
|
+
|
|
507
|
+
export type CreateContactInput = z.infer<typeof createContactSchema>;
|
|
508
|
+
export type UpdateContactInput = z.infer<typeof updateContactSchema>;
|
|
509
|
+
export type MergeContactsInput = z.infer<typeof mergeContactsSchema>;
|
|
510
|
+
export type SearchContactInput = z.infer<typeof searchContactSchema>;
|
|
511
|
+
export type BulkImportContactInput = z.infer<typeof bulkImportContactSchema>;
|
|
512
|
+
export type ContactPreferences = z.infer<typeof contactPreferencesSchema>;
|
|
513
|
+
export type NotificationPreferences = z.infer<typeof notificationPreferencesSchema>;
|
|
514
|
+
export type CommunicationPreferences = z.infer<typeof communicationPreferencesSchema>;
|
|
515
|
+
|
|
516
|
+
// ============================================
|
|
517
|
+
// Validation Helper Functions
|
|
518
|
+
// ============================================
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Validate email address
|
|
522
|
+
* @param email - Email string to validate
|
|
523
|
+
* @returns Validation result with success status and normalized email or error
|
|
524
|
+
*/
|
|
525
|
+
export function validateEmail(email: string): z.SafeParseReturnType<string, string> {
|
|
526
|
+
return emailSchema.safeParse(email);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Validate phone number
|
|
531
|
+
* @param phone - Phone string to validate
|
|
532
|
+
* @returns Validation result with success status and normalized phone or error
|
|
533
|
+
*/
|
|
534
|
+
export function validatePhone(phone: string): z.SafeParseReturnType<string, string> {
|
|
535
|
+
return phoneSchema.safeParse(phone);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Validate contact name
|
|
540
|
+
* @param firstName - First name to validate
|
|
541
|
+
* @param lastName - Last name to validate
|
|
542
|
+
* @returns Object with validation results for both names
|
|
543
|
+
*/
|
|
544
|
+
export function validateContactName(
|
|
545
|
+
firstName: string,
|
|
546
|
+
lastName: string
|
|
547
|
+
): {
|
|
548
|
+
firstName: z.SafeParseReturnType<string, string>;
|
|
549
|
+
lastName: z.SafeParseReturnType<string, string>;
|
|
550
|
+
isValid: boolean;
|
|
551
|
+
} {
|
|
552
|
+
const firstNameResult = firstNameSchema.safeParse(firstName);
|
|
553
|
+
const lastNameResult = lastNameSchema.safeParse(lastName);
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
firstName: firstNameResult,
|
|
557
|
+
lastName: lastNameResult,
|
|
558
|
+
isValid: firstNameResult.success && lastNameResult.success,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Validate create contact input
|
|
564
|
+
* @param input - Contact input to validate
|
|
565
|
+
* @returns Validation result with success status and validated data or error
|
|
566
|
+
*/
|
|
567
|
+
export function validateCreateContact(
|
|
568
|
+
input: unknown
|
|
569
|
+
): z.SafeParseReturnType<unknown, CreateContactInput> {
|
|
570
|
+
return createContactSchema.safeParse(input);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Validate update contact input
|
|
575
|
+
* @param input - Contact update input to validate
|
|
576
|
+
* @returns Validation result with success status and validated data or error
|
|
577
|
+
*/
|
|
578
|
+
export function validateUpdateContact(
|
|
579
|
+
input: unknown
|
|
580
|
+
): z.SafeParseReturnType<unknown, UpdateContactInput> {
|
|
581
|
+
return updateContactSchema.safeParse(input);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Format Zod errors into user-friendly message
|
|
586
|
+
* @param error - Zod error object
|
|
587
|
+
* @returns Formatted error message string
|
|
588
|
+
*/
|
|
589
|
+
export function formatValidationErrors(error: z.ZodError): string {
|
|
590
|
+
return error.issues
|
|
591
|
+
.map((issue) => {
|
|
592
|
+
const path = issue.path.join(".");
|
|
593
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
594
|
+
})
|
|
595
|
+
.join("; ");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Get validation error details as array
|
|
600
|
+
* @param error - Zod error object
|
|
601
|
+
* @returns Array of error details with path and message
|
|
602
|
+
*/
|
|
603
|
+
export function getValidationErrorDetails(
|
|
604
|
+
error: z.ZodError
|
|
605
|
+
): Array<{ path: string; message: string; code: string }> {
|
|
606
|
+
return error.issues.map((issue) => ({
|
|
607
|
+
path: issue.path.join("."),
|
|
608
|
+
message: issue.message,
|
|
609
|
+
code: issue.code,
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Module
|
|
3
|
+
*
|
|
4
|
+
* Exports all Zod validation schemas and utilities for the data package.
|
|
5
|
+
*
|
|
6
|
+
* @module @htlkg/data/validation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Contact validation schemas and utilities
|
|
10
|
+
export {
|
|
11
|
+
// Field-level validators
|
|
12
|
+
emailSchema,
|
|
13
|
+
optionalEmailSchema,
|
|
14
|
+
phoneSchema,
|
|
15
|
+
optionalPhoneSchema,
|
|
16
|
+
firstNameSchema,
|
|
17
|
+
lastNameSchema,
|
|
18
|
+
optionalFirstNameSchema,
|
|
19
|
+
optionalLastNameSchema,
|
|
20
|
+
localeSchema,
|
|
21
|
+
optionalLocaleSchema,
|
|
22
|
+
isoDateStringSchema,
|
|
23
|
+
optionalIsoDateStringSchema,
|
|
24
|
+
uuidSchema,
|
|
25
|
+
brandIdSchema,
|
|
26
|
+
tagSchema,
|
|
27
|
+
tagsArraySchema,
|
|
28
|
+
// Custom field schemas
|
|
29
|
+
CustomFieldType,
|
|
30
|
+
customFieldDefinitionSchema,
|
|
31
|
+
createCustomFieldValueSchema,
|
|
32
|
+
contactPreferencesSchema,
|
|
33
|
+
notificationPreferencesSchema,
|
|
34
|
+
communicationPreferencesSchema,
|
|
35
|
+
// Additional entity schemas (not exported from mutations)
|
|
36
|
+
searchContactSchema,
|
|
37
|
+
bulkImportContactSchema,
|
|
38
|
+
// Validation helper functions
|
|
39
|
+
validateEmail,
|
|
40
|
+
validatePhone,
|
|
41
|
+
validateContactName,
|
|
42
|
+
validateCreateContact,
|
|
43
|
+
validateUpdateContact,
|
|
44
|
+
formatValidationErrors,
|
|
45
|
+
getValidationErrorDetails,
|
|
46
|
+
// Types (only types not already exported from mutations)
|
|
47
|
+
type CustomFieldTypeValue,
|
|
48
|
+
type CustomFieldDefinition,
|
|
49
|
+
type SearchContactInput,
|
|
50
|
+
type BulkImportContactInput,
|
|
51
|
+
type ContactPreferences,
|
|
52
|
+
type NotificationPreferences,
|
|
53
|
+
type CommunicationPreferences,
|
|
54
|
+
} from "./contact.schemas";
|