UNPKG

ak-zod-form-kit

Version:

A powerful utility library for processing, transforming, and validating form data with Zod.

229 lines (228 loc) 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createDynamicZodSchema = createDynamicZodSchema; const zod_1 = require("zod"); /** * Crée un schéma Zod dynamique avec un contrôle précis de la validation. * * L'ordre des opérations est CRITIQUE : * 1. D'abord les modifications structurelles (merge/union) * 2. Ensuite les stratégies de validation * * @param baseSchema Schéma Zod de base * @param inputData Données pour guider les champs dynamiques (optionnel) * @param validationStrategy Stratégie de validation (défaut: "strict") * @param schemaModification Modification structurelle (défaut: "default") * @param additionalSchemas Schémas supplémentaires pour fusion (optionnel) * * @example * // Cas de base : validation stricte * // Accepte uniquement un objet avec un champ 'name' de type string. * // Tout champ supplémentaire provoquera une erreur. * const strictSchema = createDynamicZodSchema( * z.object({ name: z.string() }) * ); * * @example * // Cas avec suppression des champs supplémentaires * // Seuls les champs 'name' et 'age' seront conservés après validation. * const removeExtraSchema = createDynamicZodSchema( * z.object({ name: z.string(), age: z.number() }), * {}, // inputData n'est pas pertinent ici * "removeExtraFields" * ); * // const parsedData = removeExtraSchema.parse({ name: "Alice", age: 30, extra: "field" }); * // console.log(parsedData); // { name: "Alice", age: 30 } * * @example * // Cas avec autorisation des champs supplémentaires * // Le schéma de base accepte 'name', mais 'inputData' contient 'id'. * // Le schéma résultant acceptera 'name' et 'id' (typé comme unknown), et tout autre champ. * const allowExtraSchema = createDynamicZodSchema( * z.object({ name: z.string() }), * { name: "Bob", id: 123 }, // inputData pour guider les champs dynamiques * "allowExtraFields" * ); * // const parsedData = allowExtraSchema.parse({ name: "Charlie", id: 456, other: "data" }); * // console.log(parsedData); // { name: "Charlie", id: 456, other: "data" } * * @example * // Cas avec rendu partiel et strict ("partial-strict") * // Permet à 'name' et 'email' d'être optionnels, mais rejette tout autre champ. * const partialStrictSchema = createDynamicZodSchema( * z.object({ name: z.string(), email: z.string().email() }), * {}, * "partial-strict" * ); * // const parsedData = partialStrictSchema.parse({ name: "David" }); // Valide * // const parsedData = partialStrictSchema.parse({ email: "d@example.com" }); // Valide * // try { partialStrictSchema.parse({ age: 25 }); } catch (e) { console.error(e); } // Erreur, 'age' est un champ extra * * @example * // Cas avec rendu profondément partiel ("deepPartial") * // Rend tous les champs, y compris les imbriqués, optionnels. * const deepPartialSchema = createDynamicZodSchema( * z.object({ * user: z.object({ * id: z.number(), * profile: z.object({ * name: z.string(), * age: z.number().optional() * }) * }) * }), * {}, * "deepPartial" * ); * // const parsedData1 = deepPartialSchema.parse({ user: { profile: { name: "Eve" } } }); // Valide, 'id' et 'age' sont optionnels * // const parsedData2 = deepPartialSchema.parse({ user: {} }); // Valide, tous les champs imbriqués sont optionnels * // const parsedData3 = deepPartialSchema.parse({}); // Valide, tout est optionnel * * @example * // Cas avec rendu partiel (non-récursif) ("partial") * // Rend uniquement les champs de premier niveau optionnels. * const partialSchema = createDynamicZodSchema( * z.object({ * user: z.object({ * id: z.number(), * profile: z.object({ * name: z.string(), * age: z.number() * }) * }) * }), * {}, * "partial" * ); * // const parsedData1 = partialSchema.parse({ user: { id: 1, profile: { name: "Frank", age: 30 } } }); // Valide * // const parsedData2 = partialSchema.parse({ user: { profile: { name: "Grace", age: 25 } } }); // Valide, 'id' est manquant mais 'user' est optionnel * // try { partialSchema.parse({ user: { id: 1, profile: { name: "Henry" } } }); } catch (e) { console.error(e); } // Erreur, 'age' est requis dans 'profile' * * @example * // Cas de fusion avec logique ET ("mergeWithAnd") * // Combine un schéma de base avec un ou plusieurs schémas supplémentaires. * // Les données doivent satisfaire TOUS les schémas fusionnés. * const schemaPart1 = z.object({ id: z.number() }); * const schemaPart2 = z.object({ name: z.string() }); * const mergedAndSchema = createDynamicZodSchema( * schemaPart1, * {}, * "strict", // La stratégie de validation s'applique au schéma fusionné * "mergeWithAnd", * [schemaPart2, z.object({ isActive: z.boolean() })] * ); * // Ce schéma attendra { id: number, name: string, isActive: boolean }. * // const parsedData = mergedAndSchema.parse({ id: 1, name: "Frank", isActive: true }); // Valide * // try { mergedAndSchema.parse({ id: 1, name: "Frank" }); } catch (e) { console.error(e); } // Erreur, 'isActive' est manquant * * @example * // Cas de fusion avec logique OU ("mergeWithOr") * // Crée une union de schémas. Les données doivent satisfaire AU MOINS UN des schémas. * const schemaOption1 = z.object({ type: z.literal("car"), wheels: z.number() }); * const schemaOption2 = z.object({ type: z.literal("boat"), sails: z.number() }); * const mergedOrSchema = createDynamicZodSchema( * schemaOption1, * {}, * "strict", * "mergeWithOr", * [schemaOption2] * ); * // const parsedData1 = mergedOrSchema.parse({ type: "car", wheels: 4 }); // Valide * // const parsedData2 = mergedOrSchema.parse({ type: "boat", sails: 1 }); // Valide * // try { mergedOrSchema.parse({ type: "plane", wings: 2 }); } catch (e) { console.error(e); } // Erreur */ function createDynamicZodSchema(baseSchema, inputData = {}, validationStrategy = 'strict', schemaModification = 'default', additionalSchemas = []) { // `schema` sera la référence mutable du schéma en cours de construction. // Initialisé comme ZodTypeAny pour permettre sa réaffectation à différents types Zod (ZodObject, ZodUnion, etc.). let schema = baseSchema; // --- // 1. MODIFICATIONS STRUCTURELLES // (Doivent être appliquées en premier car elles définissent la forme fondamentale du schéma) // --- switch (schemaModification) { case 'mergeWithAnd': { // Fusionne le schéma de base avec tous les schémas supplémentaires fournis. // `reduce` est utilisé pour appliquer `merge` séquentiellement, combinant les champs de tous les schémas. // Si plusieurs schémas définissent le même champ, le dernier schéma dans la chaîne de fusion "gagne" en termes de type. // Les casts `as ZodObject<ZodRawShape>` sont nécessaires pour informer TypeScript que // `acc` et `curr` sont bien des objets Zod lors de l'appel à `merge`. schema = additionalSchemas.reduce((acc, curr) => acc.merge(curr), schema); break; } case 'mergeWithOr': { // Crée une union entre le schéma de base et tous les schémas supplémentaires. // Les données validées doivent correspondre à la forme de l'un des schémas fournis dans l'union. schema = zod_1.z.union([schema, ...additionalSchemas]); break; } case 'default': // Aucune modification structurelle n'est appliquée. Le `baseSchema` est utilisé tel quel pour la prochaine étape. break; } // --- // 2. STRATÉGIES DE VALIDATION // (Appliquées après les modifications structurelles pour affiner le comportement de validation) // --- // IMPORTANT : Avant d'appliquer les méthodes spécifiques aux objets (comme .passthrough(), .strip(), etc.), // nous devons nous assurer que `schema` est bien une instance de `ZodObject`. // Si `schema` est un `ZodUnion` (résultant d'un `mergeWithOr`), ces méthodes n'existeront pas. if (schema instanceof zod_1.ZodObject) { // Crée une nouvelle constante `objectSchema` qui est explicitement typée comme `ZodObject<any>`. // Cela permet à TypeScript de savoir avec certitude qu'il s'agit d'un objet Zod, // même dans les contextes imbriqués (comme les callbacks de `filter` ou `reduce`), // évitant ainsi les erreurs de type `Property 'shape' does not exist` ou `Property 'extend' does not exist`. const objectSchema = schema; // Utilise `any` pour la forme afin d'éviter des génériques complexes. switch (validationStrategy) { case 'allowExtraFields': { // `.passthrough()` : Permet à Zod d'accepter des champs qui ne sont pas définis dans le schéma sans générer d'erreur. // Ces champs supplémentaires seront inclus dans le résultat de la validation. schema = objectSchema.passthrough(); // Utilise la variable `objectSchema` // Identifie les champs dynamiques (ceux dans `inputData` qui ne sont pas dans le schéma existant). const dynamicFields = Object.keys(inputData) .filter((key) => !(key in objectSchema.shape)) // Utilise `objectSchema.shape` pour l'accès sécurisé .reduce((acc, key) => ({ ...acc, [key]: zod_1.z.unknown() }), {}); // Si des champs dynamiques ont été identifiés, étend le schéma avec ces champs. // Bien que `passthrough()` permette déjà ces champs, `extend` les rend "connus" du schéma, // ce qui peut être utile pour l'inférence de type ou pour les outils de débogage. if (Object.keys(dynamicFields).length > 0) { schema = objectSchema.extend(dynamicFields); // Utilise `objectSchema.extend` } break; } case 'removeExtraFields': { // `.strip()` : Configure Zod pour supprimer (stripper) silencieusement tous les champs des données // qui ne sont pas explicitement définis dans le schéma. Les données validées ne contiendront que les champs connus. schema = objectSchema.strip(); // Utilise `objectSchema` break; } case 'partial-strict': { // `.partial()` : Rend tous les champs du schéma de niveau racine optionnels. // `.strict()` : Appliqué après `.partial()`, il garantit qu'aucun champ supplémentaire (non défini dans le schéma original) n'est autorisé. // C'est utile pour les mises à jour partielles où l'on veut un contrôle strict sur les champs modifiables. schema = objectSchema.partial().strict(); // Utilise `objectSchema` break; } case 'partial': { // `.partial()` : Rend tous les champs du schéma de niveau racine optionnels (non récursif). // Fix: Also allow extra fields for 'partial' strategy as per README description schema = objectSchema.partial().passthrough(); // Utilise `objectSchema` break; } case 'strict': default: { // `.strict()` : C'est le comportement par défaut pour les objets Zod, mais il est explicitement appliqué ici // pour garantir que seuls les champs définis dans le schéma sont acceptés. Tout champ supplémentaire entraînera une erreur de validation. schema = objectSchema.strict(); // Utilise `objectSchema` break; } } } else if (validationStrategy !== 'strict') { // Si le `schema` n'est plus un `ZodObject` (par exemple, c'est un `ZodUnion` suite à `mergeWithOr`) // et qu'une stratégie de validation non-stricte est demandée, nous émettons un avertissement. // En effet, les stratégies comme `passthrough`, `strip`, `partial` ne s'appliquent qu'aux `ZodObject`. // Un `ZodUnion` se comporte intrinsèquement de manière "stricte" par rapport aux schémas qu'il contient. console.warn(`Attention : La stratégie de validation '${validationStrategy}' est généralement pour les ZodObjects, mais le schéma est maintenant un ZodUnion. Il se comportera comme 'strict'.`); } return schema; }