@kravc/schema 2.7.6 → 2.8.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +19 -14
  2. package/dist/CredentialFactory.d.ts +345 -0
  3. package/dist/CredentialFactory.d.ts.map +1 -0
  4. package/dist/CredentialFactory.js +381 -0
  5. package/dist/CredentialFactory.js.map +1 -0
  6. package/dist/Schema.d.ts +448 -0
  7. package/dist/Schema.d.ts.map +1 -0
  8. package/dist/Schema.js +506 -0
  9. package/dist/Schema.js.map +1 -0
  10. package/dist/ValidationError.d.ts +70 -0
  11. package/dist/ValidationError.d.ts.map +1 -0
  12. package/dist/ValidationError.js +78 -0
  13. package/dist/ValidationError.js.map +1 -0
  14. package/dist/Validator.d.ts +483 -0
  15. package/dist/Validator.d.ts.map +1 -0
  16. package/dist/Validator.js +570 -0
  17. package/dist/Validator.js.map +1 -0
  18. package/dist/helpers/JsonSchema.d.ts +99 -0
  19. package/dist/helpers/JsonSchema.d.ts.map +1 -0
  20. package/dist/helpers/JsonSchema.js +3 -0
  21. package/dist/helpers/JsonSchema.js.map +1 -0
  22. package/dist/helpers/cleanupAttributes.d.ts +34 -0
  23. package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
  24. package/dist/helpers/cleanupAttributes.js +113 -0
  25. package/dist/helpers/cleanupAttributes.js.map +1 -0
  26. package/dist/helpers/cleanupNulls.d.ts +27 -0
  27. package/dist/helpers/cleanupNulls.d.ts.map +1 -0
  28. package/dist/helpers/cleanupNulls.js +96 -0
  29. package/dist/helpers/cleanupNulls.js.map +1 -0
  30. package/dist/helpers/createSchemasMap.d.ts +67 -0
  31. package/dist/helpers/createSchemasMap.d.ts.map +1 -0
  32. package/dist/helpers/createSchemasMap.js +200 -0
  33. package/dist/helpers/createSchemasMap.js.map +1 -0
  34. package/dist/helpers/getReferenceIds.d.ts +169 -0
  35. package/dist/helpers/getReferenceIds.d.ts.map +1 -0
  36. package/dist/helpers/getReferenceIds.js +241 -0
  37. package/dist/helpers/getReferenceIds.js.map +1 -0
  38. package/dist/helpers/got.d.ts +60 -0
  39. package/dist/helpers/got.d.ts.map +1 -0
  40. package/dist/helpers/got.js +72 -0
  41. package/dist/helpers/got.js.map +1 -0
  42. package/dist/helpers/mapObjectProperties.d.ts +150 -0
  43. package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
  44. package/dist/helpers/mapObjectProperties.js +229 -0
  45. package/dist/helpers/mapObjectProperties.js.map +1 -0
  46. package/dist/helpers/normalizeAttributes.d.ts +213 -0
  47. package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
  48. package/dist/helpers/normalizeAttributes.js +243 -0
  49. package/dist/helpers/normalizeAttributes.js.map +1 -0
  50. package/dist/helpers/normalizeProperties.d.ts +168 -0
  51. package/dist/helpers/normalizeProperties.d.ts.map +1 -0
  52. package/dist/helpers/normalizeProperties.js +223 -0
  53. package/dist/helpers/normalizeProperties.js.map +1 -0
  54. package/dist/helpers/normalizeRequired.d.ts +159 -0
  55. package/dist/helpers/normalizeRequired.d.ts.map +1 -0
  56. package/dist/helpers/normalizeRequired.js +206 -0
  57. package/dist/helpers/normalizeRequired.js.map +1 -0
  58. package/dist/helpers/normalizeType.d.ts +81 -0
  59. package/dist/helpers/normalizeType.d.ts.map +1 -0
  60. package/dist/helpers/normalizeType.js +210 -0
  61. package/dist/helpers/normalizeType.js.map +1 -0
  62. package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
  63. package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
  64. package/dist/helpers/nullifyEmptyValues.js +191 -0
  65. package/dist/helpers/nullifyEmptyValues.js.map +1 -0
  66. package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
  67. package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
  68. package/dist/helpers/removeRequiredAndDefault.js +138 -0
  69. package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
  70. package/dist/helpers/validateId.d.ts +39 -0
  71. package/dist/helpers/validateId.d.ts.map +1 -0
  72. package/dist/helpers/validateId.js +51 -0
  73. package/dist/helpers/validateId.js.map +1 -0
  74. package/dist/index.d.ts +9 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +21 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/ld/documentLoader.d.ts +8 -0
  79. package/dist/ld/documentLoader.d.ts.map +1 -0
  80. package/dist/ld/documentLoader.js +24 -0
  81. package/dist/ld/documentLoader.js.map +1 -0
  82. package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
  83. package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
  84. package/dist/ld/getLinkedDataAttributeType.js +32 -0
  85. package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
  86. package/dist/ld/getLinkedDataContext.d.ts +19 -0
  87. package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
  88. package/dist/ld/getLinkedDataContext.js +50 -0
  89. package/dist/ld/getLinkedDataContext.js.map +1 -0
  90. package/eslint.config.mjs +32 -52
  91. package/examples/credentials/createAccountCredential.ts +27 -0
  92. package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
  93. package/examples/index.ts +7 -0
  94. package/examples/schemas/FavoriteItemSchema.ts +27 -0
  95. package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
  96. package/examples/schemas/PreferencesSchema.ts +29 -0
  97. package/examples/schemas/ProfileSchema.ts +91 -0
  98. package/examples/schemas/Status.yaml +3 -0
  99. package/examples/schemas/StatusSchema.ts +12 -0
  100. package/jest.config.mjs +5 -0
  101. package/package.json +27 -20
  102. package/src/CredentialFactory.ts +392 -0
  103. package/src/Schema.ts +583 -0
  104. package/src/ValidationError.ts +90 -0
  105. package/src/Validator.ts +603 -0
  106. package/src/__tests__/CredentialFactory.test.ts +588 -0
  107. package/src/__tests__/Schema.test.ts +371 -0
  108. package/src/__tests__/ValidationError.test.ts +235 -0
  109. package/src/__tests__/Validator.test.ts +787 -0
  110. package/src/helpers/JsonSchema.ts +119 -0
  111. package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
  112. package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
  113. package/src/helpers/__tests__/createSchemasMap.test.ts +238 -0
  114. package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
  115. package/src/helpers/__tests__/got.test.ts +193 -0
  116. package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
  117. package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
  118. package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
  119. package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
  120. package/src/helpers/__tests__/normalizeType.test.ts +772 -0
  121. package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
  122. package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
  123. package/src/helpers/__tests__/validateId.test.ts +118 -0
  124. package/src/helpers/cleanupAttributes.ts +151 -0
  125. package/src/helpers/cleanupNulls.ts +106 -0
  126. package/src/helpers/createSchemasMap.ts +212 -0
  127. package/src/helpers/getReferenceIds.ts +273 -0
  128. package/src/helpers/got.ts +73 -0
  129. package/src/helpers/mapObjectProperties.ts +272 -0
  130. package/src/helpers/normalizeAttributes.ts +247 -0
  131. package/src/helpers/normalizeProperties.ts +249 -0
  132. package/src/helpers/normalizeRequired.ts +233 -0
  133. package/src/helpers/normalizeType.ts +235 -0
  134. package/src/helpers/nullifyEmptyValues.ts +207 -0
  135. package/src/helpers/removeRequiredAndDefault.ts +151 -0
  136. package/src/helpers/validateId.ts +53 -0
  137. package/src/index.ts +17 -0
  138. package/src/ld/__tests__/documentLoader.test.ts +57 -0
  139. package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
  140. package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
  141. package/src/ld/documentLoader.ts +28 -0
  142. package/src/ld/getLinkedDataAttributeType.ts +46 -0
  143. package/src/ld/getLinkedDataContext.ts +80 -0
  144. package/tsconfig.json +27 -0
  145. package/types/credentials-context.d.ts +14 -0
  146. package/types/security-context.d.ts +6 -0
  147. package/examples/Status.yaml +0 -3
  148. package/examples/createAccountCredential.js +0 -27
  149. package/examples/createMineSweeperScoreCredential.js +0 -63
  150. package/examples/index.js +0 -9
  151. package/src/CredentialFactory.js +0 -67
  152. package/src/CredentialFactory.spec.js +0 -131
  153. package/src/Schema.js +0 -104
  154. package/src/Schema.spec.js +0 -172
  155. package/src/ValidationError.js +0 -31
  156. package/src/Validator.js +0 -128
  157. package/src/Validator.spec.js +0 -355
  158. package/src/helpers/cleanupAttributes.js +0 -71
  159. package/src/helpers/cleanupNulls.js +0 -42
  160. package/src/helpers/getReferenceIds.js +0 -71
  161. package/src/helpers/mapObject.js +0 -65
  162. package/src/helpers/normalizeAttributes.js +0 -28
  163. package/src/helpers/normalizeProperties.js +0 -61
  164. package/src/helpers/normalizeRequired.js +0 -37
  165. package/src/helpers/normalizeType.js +0 -41
  166. package/src/helpers/nullifyEmptyValues.js +0 -57
  167. package/src/helpers/removeRequiredAndDefault.js +0 -30
  168. package/src/helpers/validateId.js +0 -19
  169. package/src/index.d.ts +0 -25
  170. package/src/index.js +0 -8
  171. package/src/ld/documentLoader.js +0 -25
  172. package/src/ld/documentLoader.spec.js +0 -12
  173. package/src/ld/getLinkedDataContext.js +0 -63
  174. package/src/ld/getLinkedDataType.js +0 -38
  175. /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
  176. /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
@@ -0,0 +1,233 @@
1
+ import { isUndefined } from 'lodash';
2
+
3
+ import type {
4
+ JsonSchema,
5
+ EnumSchema,
6
+ ObjectSchema,
7
+ ArrayPropertySchema,
8
+ ObjectPropertySchema,
9
+ ReferencePropertySchema,
10
+ } from './JsonSchema';
11
+
12
+ /**
13
+ * Normalizes required field declarations in JSON schemas by converting property-level
14
+ * `required` flags to schema-level `required` arrays and `x-required` metadata flags.
15
+ *
16
+ * **Intent:**
17
+ * This function transforms JSON schemas from a property-centric required field model
18
+ * (where each property has its own `required: true/false` flag) to the standard JSON Schema
19
+ * format (where required fields are listed in a top-level `required` array). This normalization
20
+ * ensures compatibility with JSON Schema validators while preserving the original required
21
+ * field information through the `x-required` extension attribute.
22
+ *
23
+ * **Use Cases:**
24
+ * 1. **Schema Standardization**: Convert custom schema formats to standard JSON Schema format
25
+ * for validator compatibility
26
+ * 2. **Schema Transformation**: Prepare schemas for validation libraries that expect
27
+ * required fields in array format
28
+ * 3. **Metadata Preservation**: Maintain required field information in both standard format
29
+ * (`required` array) and extension format (`x-required` flag) for different use cases
30
+ * 4. **Schema Processing Pipeline**: Normalize schemas before validation, credential generation,
31
+ * or API documentation generation
32
+ *
33
+ * **Behavior:**
34
+ * - Mutates the input schema in-place
35
+ * - Moves `required: true` from property level to schema-level `required` array
36
+ * - Sets `x-required: true` on properties that were marked as required
37
+ * - Deletes the `required` property from individual property schemas
38
+ * - Recursively processes nested object properties
39
+ * - Recursively processes array items (including nested objects within arrays)
40
+ * - Skips reference properties (`$ref`) as they are resolved elsewhere
41
+ * - Skips EnumSchema (returns early)
42
+ * - Only sets `required` array if at least one field is required
43
+ *
44
+ * **Examples:**
45
+ *
46
+ * ```typescript
47
+ * // Example 1: Simple object schema
48
+ * const schema = {
49
+ * id: 'User',
50
+ * properties: {
51
+ * name: { type: 'string', required: true },
52
+ * email: { type: 'string', required: true },
53
+ * age: { type: 'number', required: false }
54
+ * }
55
+ * };
56
+ *
57
+ * normalizeRequired(schema);
58
+ * // Result:
59
+ * // schema.required = ['name', 'email']
60
+ * // schema.properties.name['x-required'] = true
61
+ * // schema.properties.email['x-required'] = true
62
+ * // schema.properties.name.required = undefined (deleted)
63
+ * // schema.properties.email.required = undefined (deleted)
64
+ * ```
65
+ *
66
+ * ```typescript
67
+ * // Example 2: Nested objects
68
+ * const schema = {
69
+ * id: 'Order',
70
+ * properties: {
71
+ * user: {
72
+ * type: 'object',
73
+ * required: true,
74
+ * properties: {
75
+ * name: { type: 'string', required: true },
76
+ * address: {
77
+ * type: 'object',
78
+ * properties: {
79
+ * street: { type: 'string', required: true }
80
+ * }
81
+ * }
82
+ * }
83
+ * }
84
+ * }
85
+ * };
86
+ *
87
+ * normalizeRequired(schema);
88
+ * // Result:
89
+ * // schema.required = ['user']
90
+ * // schema.properties.user['x-required'] = true
91
+ * // schema.properties.user.required = ['name']
92
+ * // schema.properties.user.properties.name['x-required'] = true
93
+ * // schema.properties.user.properties.address.required = ['street']
94
+ * // schema.properties.user.properties.address.properties.street['x-required'] = true
95
+ * ```
96
+ *
97
+ * ```typescript
98
+ * // Example 3: Arrays with object items
99
+ * const schema = {
100
+ * id: 'Order',
101
+ * properties: {
102
+ * items: {
103
+ * type: 'array',
104
+ * required: true,
105
+ * items: {
106
+ * type: 'object',
107
+ * properties: {
108
+ * productId: { type: 'string', required: true },
109
+ * quantity: { type: 'number', required: true }
110
+ * }
111
+ * }
112
+ * }
113
+ * }
114
+ * };
115
+ *
116
+ * normalizeRequired(schema);
117
+ * // Result:
118
+ * // schema.required = ['items']
119
+ * // schema.properties.items['x-required'] = true
120
+ * // schema.properties.items.items.required = ['productId', 'quantity']
121
+ * // schema.properties.items.items.properties.productId['x-required'] = true
122
+ * // schema.properties.items.items.properties.quantity['x-required'] = true
123
+ * ```
124
+ *
125
+ * ```typescript
126
+ * // Example 4: Mixed structure
127
+ * const schema = {
128
+ * id: 'Profile',
129
+ * properties: {
130
+ * name: { type: 'string', required: true },
131
+ * address: {
132
+ * type: 'object',
133
+ * required: true,
134
+ * properties: {
135
+ * street: { type: 'string', required: true },
136
+ * city: { type: 'string' }
137
+ * }
138
+ * },
139
+ * tags: {
140
+ * type: 'array',
141
+ * items: {
142
+ * type: 'object',
143
+ * properties: {
144
+ * label: { type: 'string', required: true }
145
+ * }
146
+ * }
147
+ * }
148
+ * }
149
+ * };
150
+ *
151
+ * normalizeRequired(schema);
152
+ * // Result:
153
+ * // schema.required = ['name', 'address']
154
+ * // All nested required fields are normalized recursively
155
+ * ```
156
+ *
157
+ * **Limitations:**
158
+ * - Only processes schemas with a `properties` field (ObjectSchema or ObjectPropertySchema)
159
+ * - EnumSchema is accepted but returns early without processing
160
+ * - Reference properties (`$ref`) are skipped and not processed
161
+ * - The function mutates the input schema object
162
+ * - Does not resolve `$ref` references (they must be resolved separately)
163
+ *
164
+ * @param jsonSchema - The JSON schema to normalize (ObjectSchema, ObjectPropertySchema, or ReferencePropertySchema)
165
+ * @returns void (mutates the input schema in-place)
166
+ */
167
+ const normalizeRequired = (jsonSchema: JsonSchema | ObjectPropertySchema | ReferencePropertySchema) => {
168
+ const { enum: enumItems } = (jsonSchema as EnumSchema);
169
+
170
+ const isEnum = !!enumItems;
171
+
172
+ if (isEnum) {
173
+ return;
174
+ }
175
+
176
+ const objectSchema = (jsonSchema as ObjectSchema);
177
+ const { properties } = objectSchema;
178
+
179
+ if (!properties) {
180
+ return;
181
+ }
182
+
183
+ const required = [];
184
+
185
+ for (const propertyName in properties) {
186
+ const property = properties[propertyName];
187
+
188
+ const { $ref: refSchemaId } = (property as ReferencePropertySchema);
189
+
190
+ const isReference = !isUndefined(refSchemaId);
191
+
192
+ // Handle required flag for all properties (including references)
193
+ if (property.required) {
194
+ property['x-required'] = true;
195
+ required.push(propertyName);
196
+ }
197
+
198
+ // Delete required property for all properties (whether true or false)
199
+ delete property.required;
200
+
201
+ // Skip recursive processing for reference properties
202
+ if (isReference) {
203
+ continue;
204
+ }
205
+
206
+ const { type } = (property as ObjectPropertySchema | ArrayPropertySchema);
207
+
208
+ const isObject = type === 'object';
209
+
210
+ if (isObject) {
211
+ normalizeRequired(property as ObjectPropertySchema);
212
+ continue;
213
+ }
214
+
215
+ const isArray = type === 'array';
216
+
217
+ if (isArray) {
218
+ const { items } = (property as ArrayPropertySchema);
219
+
220
+ if (items) {
221
+ normalizeRequired(items as ObjectPropertySchema | ReferencePropertySchema);
222
+ }
223
+
224
+ continue;
225
+ }
226
+ }
227
+
228
+ if (required.length > 0) {
229
+ objectSchema.required = required;
230
+ }
231
+ };
232
+
233
+ export default normalizeRequired;
@@ -0,0 +1,235 @@
1
+ type ValueType = 'number' | 'integer' | 'boolean' | 'string' | 'object' | 'array';
2
+
3
+ const BOOLEAN_STRING_TRUE_VALUES = ['yes', 'true', '1'];
4
+ const BOOLEAN_STRING_FALSE_VALUES = ['no', 'false', '0'];
5
+
6
+ /**
7
+ * Checks if a value is null or undefined.
8
+ *
9
+ * @param value - The value to check
10
+ * @returns True if the value is null or undefined, false otherwise
11
+ */
12
+ const isNullOrUndefined = (value: unknown): value is null | undefined =>
13
+ value === null || value === undefined;
14
+
15
+ /**
16
+ * Checks if a JSON Schema type is numeric (number or integer).
17
+ *
18
+ * @param type - The JSON Schema type to check
19
+ * @returns True if the type is 'number' or 'integer', false otherwise
20
+ */
21
+ const isNumericType = (type: ValueType): boolean => type === 'number' || type === 'integer';
22
+
23
+ /**
24
+ * Checks if a JSON Schema type is boolean.
25
+ *
26
+ * @param type - The JSON Schema type to check
27
+ * @returns True if the type is 'boolean', false otherwise
28
+ */
29
+ const isBooleanType = (type: ValueType): boolean => type === 'boolean';
30
+
31
+ /**
32
+ * Type guard that checks if a value is a number.
33
+ *
34
+ * @param value - The value to check
35
+ * @returns True if the value is a number, false otherwise
36
+ */
37
+ const isNumberValue = (value: unknown): value is number => typeof value === 'number';
38
+
39
+ /**
40
+ * Type guard that checks if a value is a boolean.
41
+ *
42
+ * @param value - The value to check
43
+ * @returns True if the value is a boolean, false otherwise
44
+ */
45
+ const isBooleanValue = (value: unknown): value is boolean => typeof value === 'boolean';
46
+
47
+ /**
48
+ * Type guard that checks if a value is a string.
49
+ *
50
+ * @param value - The value to check
51
+ * @returns True if the value is a string, false otherwise
52
+ */
53
+ const isStringValue = (value: unknown): value is string => typeof value === 'string';
54
+
55
+ /**
56
+ * Checks if a string is empty or contains only whitespace characters.
57
+ *
58
+ * @param value - The string to check
59
+ * @returns True if the string is empty or whitespace-only, false otherwise
60
+ */
61
+ const isEmptyOrWhitespaceString = (value: string): boolean =>
62
+ value === '' || value.trim() === '';
63
+
64
+ /**
65
+ * Checks if a number is valid (not NaN).
66
+ *
67
+ * @param value - The number to check
68
+ * @returns True if the number is valid (not NaN), false otherwise
69
+ */
70
+ const isValidNumber = (value: number): boolean => !isNaN(value);
71
+
72
+ /**
73
+ * Checks if a string represents a boolean true value.
74
+ * Recognized values (case-insensitive): 'yes', 'true', '1'.
75
+ *
76
+ * @param value - The string to check
77
+ * @returns True if the string represents a boolean true value, false otherwise
78
+ */
79
+ const isBooleanTrueString = (value: string): boolean =>
80
+ BOOLEAN_STRING_TRUE_VALUES.includes(value.toLowerCase());
81
+
82
+ /**
83
+ * Checks if a string represents a boolean false value.
84
+ * Recognized values (case-insensitive): 'no', 'false', '0'.
85
+ *
86
+ * @param value - The string to check
87
+ * @returns True if the string represents a boolean false value, false otherwise
88
+ */
89
+ const isBooleanFalseString = (value: string): boolean =>
90
+ BOOLEAN_STRING_FALSE_VALUES.includes(value.toLowerCase());
91
+
92
+ /**
93
+ * Normalizes a value to match a specified JSON Schema type.
94
+ *
95
+ * ## Intent
96
+ *
97
+ * This function is designed to coerce values into their expected types based on JSON Schema
98
+ * type definitions. It's particularly useful when processing data from external sources (like
99
+ * form inputs, query parameters, or API responses) where values may arrive as strings but
100
+ * need to be converted to their proper types according to a schema definition.
101
+ *
102
+ * The function performs type coercion where appropriate, but preserves the original value
103
+ * when conversion is not possible or when the value is already of the correct type. This
104
+ * makes it safe to use in data normalization pipelines without losing information.
105
+ *
106
+ * ## Use Cases
107
+ *
108
+ * 1. **Schema-based data normalization**: When processing objects against JSON Schema
109
+ * definitions, ensuring property values match their declared types.
110
+ *
111
+ * 2. **Form data processing**: Converting string values from HTML forms (which are always
112
+ * strings) to their expected types (numbers, booleans) based on schema definitions.
113
+ *
114
+ * 3. **API response normalization**: Normalizing API responses where types may be ambiguous
115
+ * or incorrectly serialized (e.g., numbers as strings, booleans as strings).
116
+ *
117
+ * 4. **Configuration parsing**: Parsing configuration values from environment variables or
118
+ * config files where everything is initially a string but needs type coercion.
119
+ *
120
+ * ## Behavior by Type
121
+ *
122
+ * - **number/integer**: Attempts to convert strings and booleans to numbers. Preserves
123
+ * original value if conversion fails or value is already a number.
124
+ *
125
+ * - **boolean**: Converts numbers (0 → false, non-zero → true) and recognized string
126
+ * values ('yes', 'true', '1' → true; 'no', 'false', '0' → false). Preserves original
127
+ * value for unrecognized strings or non-convertible types.
128
+ *
129
+ * - **string/object/array**: Returns the value as-is (no conversion performed).
130
+ *
131
+ * - **null/undefined**: Always preserved regardless of target type.
132
+ *
133
+ * ## Examples
134
+ *
135
+ * ### Number Conversion
136
+ * ```typescript
137
+ * normalizeType('number', '123') // → 123
138
+ * normalizeType('number', '45.67') // → 45.67
139
+ * normalizeType('number', '0') // → 0
140
+ * normalizeType('number', true) // → 1
141
+ * normalizeType('number', 'abc') // → 'abc' (conversion failed, original preserved)
142
+ * ```
143
+ *
144
+ * ### Boolean Conversion
145
+ * ```typescript
146
+ * normalizeType('boolean', 0) // → false
147
+ * normalizeType('boolean', 1) // → true
148
+ * normalizeType('boolean', 'yes') // → true
149
+ * normalizeType('boolean', 'true') // → true
150
+ * normalizeType('boolean', '1') // → true
151
+ * normalizeType('boolean', 'no') // → false
152
+ * normalizeType('boolean', 'false') // → false
153
+ * normalizeType('boolean', 'maybe') // → 'maybe' (unrecognized, original preserved)
154
+ * ```
155
+ *
156
+ * ### Type Preservation
157
+ * ```typescript
158
+ * normalizeType('string', 'hello') // → 'hello'
159
+ * normalizeType('string', 123) // → 123 (no conversion for string type)
160
+ * normalizeType('object', { a: 1 }) // → { a: 1 }
161
+ * normalizeType('array', [1, 2, 3]) // → [1, 2, 3]
162
+ * normalizeType('number', null) // → null (null always preserved)
163
+ * ```
164
+ *
165
+ * @param type - The target JSON Schema type ('number', 'integer', 'boolean', 'string', 'object', 'array')
166
+ * @param value - The value to normalize (can be any type)
167
+ * @returns The normalized value, or the original value if normalization is not applicable
168
+ */
169
+ const normalizeType = (type: ValueType, value: unknown): string | number | boolean | unknown => {
170
+ // Preserve null and undefined values regardless of target type
171
+ if (isNullOrUndefined(value)) {
172
+ return value;
173
+ }
174
+
175
+ // Handle number and integer types
176
+ if (isNumericType(type)) {
177
+ // If already a number, return as-is
178
+ if (isNumberValue(value)) {
179
+ return value;
180
+ }
181
+
182
+ // Convert booleans to numbers: true → 1, false → 0
183
+ if (isBooleanValue(value)) {
184
+ return value ? 1 : 0;
185
+ }
186
+
187
+ // Attempt conversion for strings
188
+ if (isStringValue(value)) {
189
+ // Preserve empty strings and whitespace-only strings
190
+ if (isEmptyOrWhitespaceString(value)) {
191
+ return value;
192
+ }
193
+
194
+ const converted = Number(value);
195
+ // Check if conversion was successful (not NaN)
196
+ if (isValidNumber(converted)) {
197
+ return converted;
198
+ }
199
+ }
200
+
201
+ // Return original value if conversion failed or type is not convertible
202
+ return value;
203
+ }
204
+
205
+ // Handle boolean type
206
+ if (isBooleanType(type)) {
207
+ // If already a boolean, return as-is
208
+ if (isBooleanValue(value)) {
209
+ return value;
210
+ }
211
+
212
+ // Convert numbers: 0 → false, non-zero → true
213
+ if (isNumberValue(value)) {
214
+ return Boolean(value);
215
+ }
216
+
217
+ // Convert recognized string values
218
+ if (isStringValue(value)) {
219
+ if (isBooleanTrueString(value)) {
220
+ return true;
221
+ }
222
+ if (isBooleanFalseString(value)) {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ // Return original value for unrecognized strings or non-convertible types
228
+ return value;
229
+ }
230
+
231
+ // For string, object, and array types, return value as-is (no conversion)
232
+ return value;
233
+ };
234
+
235
+ export default normalizeType;
@@ -0,0 +1,207 @@
1
+ import { get, set } from 'lodash';
2
+ import { schemaSymbol, jsonSymbol, type SchemaErrorDetail } from 'z-schema';
3
+
4
+ import type { TargetObject } from './JsonSchema';
5
+
6
+ /**
7
+ * Format error codes that indicate validation failures due to format mismatches.
8
+ * These errors can potentially be resolved by converting empty strings to null.
9
+ */
10
+ const FORMAT_ERROR_CODES = [
11
+ 'PATTERN',
12
+ 'ENUM_MISMATCH',
13
+ 'INVALID_FORMAT'
14
+ ] as const;
15
+
16
+ /**
17
+ * Values that are considered "empty" and can be safely converted to null.
18
+ */
19
+ const EMPTY_VALUES = [''] as const;
20
+
21
+ /**
22
+ * Converts empty string values to null for specific format validation errors.
23
+ *
24
+ * **Intent:**
25
+ * This function provides a post-validation normalization strategy that attempts to
26
+ * resolve format validation errors by converting empty strings to null. This is
27
+ * particularly useful when dealing with optional fields where an empty string
28
+ * represents "no value provided" rather than an invalid format. By converting
29
+ * empty strings to null, the validation may pass if the schema allows null values
30
+ * for optional fields.
31
+ *
32
+ * **Use Cases:**
33
+ * - **Optional field normalization**: When optional string fields receive empty
34
+ * strings from user input or API responses, converting them to null allows
35
+ * validation to succeed if the schema permits null values for optional fields.
36
+ * This is especially common with form inputs that submit empty strings instead
37
+ * of omitting fields entirely.
38
+ * - **Format error recovery**: After schema validation fails with format errors
39
+ * (pattern mismatch, enum mismatch, invalid format), this function attempts to
40
+ * resolve errors by nullifying empty strings. This enables graceful handling
41
+ * of optional fields that failed format validation due to empty values.
42
+ * - **API integration**: When integrating with external APIs or services that
43
+ * send empty strings for optional fields, this function normalizes the data
44
+ * to use null instead, which is often more semantically correct for optional
45
+ * fields in JSON schemas.
46
+ * - **Data transformation pipeline**: As part of a validation and normalization
47
+ * pipeline, this function can be used to clean and normalize data before
48
+ * further processing or storage, ensuring consistent representation of
49
+ * "missing" values.
50
+ * - **User input handling**: When processing user-submitted forms or data where
51
+ * empty string inputs should be treated as "not provided" rather than invalid,
52
+ * this function converts them to null for proper schema validation.
53
+ *
54
+ * **Behavior:**
55
+ * - Returns a deep clone of the input object (does not mutate the original)
56
+ * - Only processes format-related errors (PATTERN, ENUM_MISMATCH, INVALID_FORMAT)
57
+ * - Skips required attributes (marked with `x-required: true`)
58
+ * - Only converts empty strings (`''`) to null, preserving other values
59
+ * - Supports nested paths and array indices
60
+ * - Returns both the modified object and remaining validation errors that
61
+ * couldn't be resolved
62
+ *
63
+ * **Examples:**
64
+ *
65
+ * ```typescript
66
+ * // Example 1: Basic usage with pattern error
67
+ * const object = { email: '' };
68
+ * const error = {
69
+ * code: 'PATTERN',
70
+ * path: '#/email',
71
+ * // ... other error properties
72
+ * };
73
+ * const [result, remainingErrors] = nullifyEmptyValues(object, [error]);
74
+ * // result: { email: null }
75
+ * // remainingErrors: []
76
+ * ```
77
+ *
78
+ * ```typescript
79
+ * // Example 2: Required fields are not nullified
80
+ * const object = { requiredField: '', optionalField: '' };
81
+ * const requiredError = {
82
+ * code: 'PATTERN',
83
+ * path: '#/requiredField',
84
+ * // schema has x-required: true
85
+ * };
86
+ * const optionalError = {
87
+ * code: 'PATTERN',
88
+ * path: '#/optionalField',
89
+ * // schema has no x-required or x-required: false
90
+ * };
91
+ * const [result, remainingErrors] = nullifyEmptyValues(
92
+ * object,
93
+ * [requiredError, optionalError]
94
+ * );
95
+ * // result: { requiredField: '', optionalField: null }
96
+ * // remainingErrors: [requiredError] // required field error remains
97
+ * ```
98
+ *
99
+ * ```typescript
100
+ * // Example 3: Nested paths and arrays
101
+ * const object = {
102
+ * user: {
103
+ * profile: {
104
+ * bio: '',
105
+ * tags: ['', 'tag1', '']
106
+ * }
107
+ * }
108
+ * };
109
+ * const errors = [
110
+ * { code: 'PATTERN', path: '#/user/profile/bio' },
111
+ * { code: 'INVALID_FORMAT', path: '#/user/profile/tags/0' },
112
+ * { code: 'ENUM_MISMATCH', path: '#/user/profile/tags/2' }
113
+ * ];
114
+ * const [result, remainingErrors] = nullifyEmptyValues(object, errors);
115
+ * // result: {
116
+ * // user: {
117
+ * // profile: {
118
+ * // bio: null,
119
+ * // tags: [null, 'tag1', null]
120
+ * // }
121
+ * // }
122
+ * // }
123
+ * // remainingErrors: []
124
+ * ```
125
+ *
126
+ * ```typescript
127
+ * // Example 4: Non-format errors are not processed
128
+ * const object = { field: '' };
129
+ * const formatError = { code: 'PATTERN', path: '#/field' };
130
+ * const typeError = { code: 'INVALID_TYPE', path: '#/field' };
131
+ * const [result, remainingErrors] = nullifyEmptyValues(
132
+ * object,
133
+ * [formatError, typeError]
134
+ * );
135
+ * // result: { field: null }
136
+ * // remainingErrors: [typeError] // type error remains
137
+ * ```
138
+ *
139
+ * ```typescript
140
+ * // Example 5: Non-empty values are preserved
141
+ * const object = { field: 'invalid-value' };
142
+ * const error = { code: 'PATTERN', path: '#/field' };
143
+ * const [result, remainingErrors] = nullifyEmptyValues(object, [error]);
144
+ * // result: { field: 'invalid-value' } // unchanged
145
+ * // remainingErrors: [error] // error remains
146
+ * ```
147
+ *
148
+ * @param object - The target object to process (will be deep cloned, not mutated)
149
+ * @param validationErrors - Array of schema validation errors from z-schema
150
+ * @returns A tuple containing:
151
+ * - `[0]`: Deep clone of the object with empty strings converted to null where applicable
152
+ * - `[1]`: Array of validation errors that couldn't be resolved (required fields,
153
+ * non-format errors, or errors for non-empty values)
154
+ */
155
+ const nullifyEmptyValues = (
156
+ object: TargetObject,
157
+ validationErrors: SchemaErrorDetail[]
158
+ ): [TargetObject, SchemaErrorDetail[]] => {
159
+ // Create a deep clone to avoid mutating the original object
160
+ const result = JSON.parse(JSON.stringify(object)) as TargetObject;
161
+ const remainingErrors: SchemaErrorDetail[] = [];
162
+
163
+ for (const error of validationErrors) {
164
+ const { code, path: pathString } = error;
165
+ const schema = get(error, schemaSymbol) as Record<string, unknown> | undefined;
166
+ const isAttributeRequired = schema?.['x-required'] === true;
167
+ const isFormatError = FORMAT_ERROR_CODES.includes(code as typeof FORMAT_ERROR_CODES[number]);
168
+
169
+ const shouldSkipRequiredAttribute = isAttributeRequired;
170
+ const shouldSkipNonFormatError = !isFormatError;
171
+
172
+ // Skip required attributes - they should not be nullified
173
+ if (shouldSkipRequiredAttribute) {
174
+ remainingErrors.push(error);
175
+ continue;
176
+ }
177
+
178
+ // Only process format-related errors
179
+ if (shouldSkipNonFormatError) {
180
+ remainingErrors.push(error);
181
+ continue;
182
+ }
183
+
184
+ // Parse the JSON path (e.g., '#/user/profile/field' -> ['user', 'profile', 'field'])
185
+ const path = pathString.replace(/^#\//, '').split('/').filter(Boolean);
186
+
187
+ // Get the actual value from the error's JSON context
188
+ const json = get(error, jsonSymbol) as TargetObject;
189
+ const value = get(json, path);
190
+
191
+ const isEmptyValue = EMPTY_VALUES.includes(value as typeof EMPTY_VALUES[number]);
192
+ const shouldSkipNonEmptyValue = !isEmptyValue;
193
+
194
+ // Only nullify if the value is actually empty
195
+ if (shouldSkipNonEmptyValue) {
196
+ remainingErrors.push(error);
197
+ continue;
198
+ }
199
+
200
+ // Set the value to null in the result object
201
+ set(result, path, null);
202
+ }
203
+
204
+ return [result, remainingErrors];
205
+ };
206
+
207
+ export default nullifyEmptyValues;