@kravc/schema 2.7.5 → 2.8.0-alpha.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 (170) 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/getReferenceIds.d.ts +169 -0
  31. package/dist/helpers/getReferenceIds.d.ts.map +1 -0
  32. package/dist/helpers/getReferenceIds.js +241 -0
  33. package/dist/helpers/getReferenceIds.js.map +1 -0
  34. package/dist/helpers/got.d.ts +60 -0
  35. package/dist/helpers/got.d.ts.map +1 -0
  36. package/dist/helpers/got.js +72 -0
  37. package/dist/helpers/got.js.map +1 -0
  38. package/dist/helpers/mapObjectProperties.d.ts +150 -0
  39. package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
  40. package/dist/helpers/mapObjectProperties.js +229 -0
  41. package/dist/helpers/mapObjectProperties.js.map +1 -0
  42. package/dist/helpers/normalizeAttributes.d.ts +213 -0
  43. package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
  44. package/dist/helpers/normalizeAttributes.js +243 -0
  45. package/dist/helpers/normalizeAttributes.js.map +1 -0
  46. package/dist/helpers/normalizeProperties.d.ts +168 -0
  47. package/dist/helpers/normalizeProperties.d.ts.map +1 -0
  48. package/dist/helpers/normalizeProperties.js +223 -0
  49. package/dist/helpers/normalizeProperties.js.map +1 -0
  50. package/dist/helpers/normalizeRequired.d.ts +159 -0
  51. package/dist/helpers/normalizeRequired.d.ts.map +1 -0
  52. package/dist/helpers/normalizeRequired.js +206 -0
  53. package/dist/helpers/normalizeRequired.js.map +1 -0
  54. package/dist/helpers/normalizeType.d.ts +81 -0
  55. package/dist/helpers/normalizeType.d.ts.map +1 -0
  56. package/dist/helpers/normalizeType.js +210 -0
  57. package/dist/helpers/normalizeType.js.map +1 -0
  58. package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
  59. package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
  60. package/dist/helpers/nullifyEmptyValues.js +191 -0
  61. package/dist/helpers/nullifyEmptyValues.js.map +1 -0
  62. package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
  63. package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
  64. package/dist/helpers/removeRequiredAndDefault.js +138 -0
  65. package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
  66. package/dist/helpers/validateId.d.ts +39 -0
  67. package/dist/helpers/validateId.d.ts.map +1 -0
  68. package/dist/helpers/validateId.js +51 -0
  69. package/dist/helpers/validateId.js.map +1 -0
  70. package/dist/index.d.ts +7 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +17 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/ld/documentLoader.d.ts +8 -0
  75. package/dist/ld/documentLoader.d.ts.map +1 -0
  76. package/dist/ld/documentLoader.js +24 -0
  77. package/dist/ld/documentLoader.js.map +1 -0
  78. package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
  79. package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
  80. package/dist/ld/getLinkedDataAttributeType.js +32 -0
  81. package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
  82. package/dist/ld/getLinkedDataContext.d.ts +19 -0
  83. package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
  84. package/dist/ld/getLinkedDataContext.js +50 -0
  85. package/dist/ld/getLinkedDataContext.js.map +1 -0
  86. package/eslint.config.mjs +32 -52
  87. package/examples/credentials/createAccountCredential.ts +27 -0
  88. package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
  89. package/examples/index.ts +7 -0
  90. package/examples/schemas/FavoriteItemSchema.ts +27 -0
  91. package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
  92. package/examples/schemas/PreferencesSchema.ts +29 -0
  93. package/examples/schemas/ProfileSchema.ts +91 -0
  94. package/examples/schemas/Status.yaml +3 -0
  95. package/examples/schemas/StatusSchema.ts +12 -0
  96. package/jest.config.mjs +5 -0
  97. package/package.json +28 -21
  98. package/src/CredentialFactory.ts +392 -0
  99. package/src/Schema.ts +583 -0
  100. package/src/ValidationError.ts +90 -0
  101. package/src/Validator.ts +603 -0
  102. package/src/__tests__/CredentialFactory.test.ts +588 -0
  103. package/src/__tests__/Schema.test.ts +371 -0
  104. package/src/__tests__/ValidationError.test.ts +235 -0
  105. package/src/__tests__/Validator.test.ts +787 -0
  106. package/src/helpers/JsonSchema.ts +119 -0
  107. package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
  108. package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
  109. package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
  110. package/src/helpers/__tests__/got.test.ts +193 -0
  111. package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
  112. package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
  113. package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
  114. package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
  115. package/src/helpers/__tests__/normalizeType.test.ts +772 -0
  116. package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
  117. package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
  118. package/src/helpers/__tests__/validateId.test.ts +118 -0
  119. package/src/helpers/cleanupAttributes.ts +151 -0
  120. package/src/helpers/cleanupNulls.ts +106 -0
  121. package/src/helpers/getReferenceIds.ts +273 -0
  122. package/src/helpers/got.ts +73 -0
  123. package/src/helpers/mapObjectProperties.ts +272 -0
  124. package/src/helpers/normalizeAttributes.ts +247 -0
  125. package/src/helpers/normalizeProperties.ts +249 -0
  126. package/src/helpers/normalizeRequired.ts +233 -0
  127. package/src/helpers/normalizeType.ts +235 -0
  128. package/src/helpers/nullifyEmptyValues.ts +207 -0
  129. package/src/helpers/removeRequiredAndDefault.ts +151 -0
  130. package/src/helpers/validateId.ts +53 -0
  131. package/src/index.ts +13 -0
  132. package/src/ld/__tests__/documentLoader.test.ts +57 -0
  133. package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
  134. package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
  135. package/src/ld/documentLoader.ts +28 -0
  136. package/src/ld/getLinkedDataAttributeType.ts +46 -0
  137. package/src/ld/getLinkedDataContext.ts +80 -0
  138. package/tsconfig.json +27 -0
  139. package/types/credentials-context.d.ts +14 -0
  140. package/types/security-context.d.ts +6 -0
  141. package/examples/Status.yaml +0 -3
  142. package/examples/createAccountCredential.js +0 -27
  143. package/examples/createMineSweeperScoreCredential.js +0 -63
  144. package/examples/index.js +0 -9
  145. package/src/CredentialFactory.js +0 -67
  146. package/src/CredentialFactory.spec.js +0 -131
  147. package/src/Schema.js +0 -104
  148. package/src/Schema.spec.js +0 -172
  149. package/src/ValidationError.js +0 -31
  150. package/src/Validator.js +0 -128
  151. package/src/Validator.spec.js +0 -355
  152. package/src/helpers/cleanupAttributes.js +0 -71
  153. package/src/helpers/cleanupNulls.js +0 -42
  154. package/src/helpers/getReferenceIds.js +0 -71
  155. package/src/helpers/mapObject.js +0 -65
  156. package/src/helpers/normalizeAttributes.js +0 -28
  157. package/src/helpers/normalizeProperties.js +0 -61
  158. package/src/helpers/normalizeRequired.js +0 -37
  159. package/src/helpers/normalizeType.js +0 -41
  160. package/src/helpers/nullifyEmptyValues.js +0 -57
  161. package/src/helpers/removeRequiredAndDefault.js +0 -30
  162. package/src/helpers/validateId.js +0 -19
  163. package/src/index.d.ts +0 -25
  164. package/src/index.js +0 -8
  165. package/src/ld/documentLoader.js +0 -25
  166. package/src/ld/documentLoader.spec.js +0 -12
  167. package/src/ld/getLinkedDataContext.js +0 -63
  168. package/src/ld/getLinkedDataType.js +0 -38
  169. /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
  170. /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
@@ -0,0 +1,73 @@
1
+ import { get, isUndefined } from 'lodash';
2
+
3
+ const DEFAULT_ERROR_TEMPLATE = 'Value is undefined for "$PATH"';
4
+
5
+ /**
6
+ * Safe required property access: returns a value at `path` or throws if it is `undefined`.
7
+ *
8
+ * **Intent:** Provide strict, fail-fast access to nested object properties. Unlike `lodash/get`,
9
+ * which returns `undefined` for missing keys, `got` treats missing data as an error and throws
10
+ * with a clear message including the path. Use it when the property is required and absence
11
+ * indicates a bug or invalid input.
12
+ *
13
+ * **Use cases:**
14
+ * - **Schema / config lookups:** Fetching a schema or config by ID from a map where absence
15
+ * means invalid reference (e.g. `got(schemasMap, schemaId, 'Schema "$PATH" not found')`).
16
+ * - **Validated config access:** Reading required config or options after validation, when
17
+ * you want to avoid `undefined` checks downstream.
18
+ * - **Strict data traversal:** Walking nested structures (APIs, parsed JSON) where missing
19
+ * keys should fail immediately with a descriptive error instead of propagating `undefined`.
20
+ *
21
+ * **Behavior:** Only `undefined` triggers an error. Falsy but defined values (`null`, `0`,
22
+ * `false`, `''`, `[]`, `{}`) are returned as-is. Uses lodash `get` path syntax: dot notation
23
+ * (`a.b.c`), bracket notation (`items[0]`), or mixed (`data.items[0].id`).
24
+ *
25
+ * @param object - Root object to read from.
26
+ * @param path - Lodash-style path (e.g. `'user.profile.name'`, `'items[0].id'`).
27
+ * @param errorTemplate - Error message template; `$PATH` is replaced with `path`. Default:
28
+ * `'Value is undefined for "$PATH"'`.
29
+ * @returns The value at `path`.
30
+ * @throws {Error} When the value at `path` is `undefined`.
31
+ *
32
+ * @example
33
+ * // Simple property
34
+ * got({ name: 'Jane' }, 'name');
35
+ * // => 'Jane'
36
+ *
37
+ * @example
38
+ * // Nested path
39
+ * got({ user: { profile: { role: 'admin' } } }, 'user.profile.role');
40
+ * // => 'admin'
41
+ *
42
+ * @example
43
+ * // Array index
44
+ * got({ items: ['a', 'b'] }, 'items[0]');
45
+ * // => 'a'
46
+ *
47
+ * @example
48
+ * // Falsy but defined values are returned
49
+ * got({ count: 0, enabled: false }, 'count');
50
+ * // => 0
51
+ *
52
+ * @example
53
+ * // Custom error for schema lookups
54
+ * got(schemasMap, schemaId, 'Schema "$PATH" not found');
55
+ * // => schema for schemaId, or throws with that message
56
+ *
57
+ * @example
58
+ * // Missing property throws
59
+ * got({ name: 'Jane' }, 'age');
60
+ * // throws Error('Value is undefined for "age"')
61
+ */
62
+ function got<T>(object: Record<string, T>, path: string, errorTemplate: string = DEFAULT_ERROR_TEMPLATE): T {
63
+ const value = get(object, path);
64
+ const shouldThrow = isUndefined(value);
65
+
66
+ if (!shouldThrow) {
67
+ return value;
68
+ }
69
+
70
+ throw Error(errorTemplate.replace('$PATH', path));
71
+ };
72
+
73
+ export default got;
@@ -0,0 +1,272 @@
1
+ import { isUndefined } from 'lodash';
2
+
3
+ import got from './got';
4
+ import type {
5
+ JsonSchema,
6
+ EnumSchema,
7
+ ObjectSchema,
8
+ TargetObject,
9
+ PropertySchema,
10
+ JsonSchemasMap,
11
+ ArrayPropertySchema,
12
+ ObjectPropertySchema,
13
+ ReferencePropertySchema,
14
+ } from './JsonSchema';
15
+
16
+ /**
17
+ * Recursively traverses an object's properties based on a JSON schema and applies a callback
18
+ * function to each property. Handles nested objects, arrays, and schema references ($ref).
19
+ *
20
+ * **Intent:**
21
+ * This function provides a generic way to iterate over object properties in a schema-aware manner,
22
+ * enabling operations like normalization, validation, transformation, or cleanup to be applied
23
+ * consistently across complex nested data structures. It abstracts away the complexity of
24
+ * traversing nested objects, arrays, and schema references, allowing callers to focus on
25
+ * implementing their specific property-level logic.
26
+ *
27
+ * **Use Cases:**
28
+ * - **Normalization**: Apply type conversions or default values to properties based on schema definitions
29
+ * (see `normalizeAttributes.ts` for example)
30
+ * - **Validation**: Check property values against schema constraints
31
+ * - **Transformation**: Modify or transform property values based on schema metadata
32
+ * - **Cleanup**: Remove invalid properties or sanitize data structures
33
+ * - **Data Processing**: Extract, aggregate, or analyze properties across nested structures
34
+ * - **Schema-driven Operations**: Any operation that needs to process object properties according
35
+ * to their schema definitions
36
+ *
37
+ * **Behavior:**
38
+ * - Skips enum schemas (returns immediately without calling callback)
39
+ * - Calls callback for all properties defined in the schema, even if their values are null
40
+ * - Skips recursion into undefined values (callback is still called, but nested traversal stops)
41
+ * - Recursively processes nested objects by creating nested schema contexts
42
+ * - Recursively processes array items, handling both inline object schemas and references
43
+ * - Resolves schema references ($ref) using the provided schemasMap
44
+ *
45
+ * **Examples:**
46
+ *
47
+ * @example
48
+ * // Example 1: Normalize property values based on schema types
49
+ * const schema = new Schema({
50
+ * name: { type: 'string' },
51
+ * age: { type: 'number' },
52
+ * active: { type: 'boolean' }
53
+ * }, 'user-schema');
54
+ *
55
+ * const user = {
56
+ * name: 'John',
57
+ * age: '30', // string that should be number
58
+ * active: 'true' // string that should be boolean
59
+ * };
60
+ *
61
+ * mapObjectProperties(user, schema.jsonSchema, {}, (propName, propSchema, obj) => {
62
+ * if (propSchema.type === 'number') {
63
+ * obj[propName] = Number(obj[propName]);
64
+ * } else if (propSchema.type === 'boolean') {
65
+ * obj[propName] = obj[propName] === 'true' || obj[propName] === true;
66
+ * }
67
+ * });
68
+ * // Result: { name: 'John', age: 30, active: true }
69
+ *
70
+ * @example
71
+ * // Example 2: Process nested objects
72
+ * const schema = new Schema({
73
+ * profile: {
74
+ * type: 'object',
75
+ * properties: {
76
+ * firstName: { type: 'string' },
77
+ * lastName: { type: 'string' }
78
+ * }
79
+ * }
80
+ * }, 'user-schema');
81
+ *
82
+ * const user = {
83
+ * profile: {
84
+ * firstName: 'John',
85
+ * lastName: 'Doe'
86
+ * }
87
+ * };
88
+ *
89
+ * const processedProps: string[] = [];
90
+ * mapObjectProperties(user, schema.jsonSchema, {}, (propName) => {
91
+ * processedProps.push(propName);
92
+ * });
93
+ * // processedProps: ['profile', 'firstName', 'lastName']
94
+ *
95
+ * @example
96
+ * // Example 3: Handle schema references ($ref)
97
+ * const addressSchema = new Schema({
98
+ * street: { type: 'string' },
99
+ * city: { type: 'string' }
100
+ * }, 'address-schema');
101
+ *
102
+ * const userSchema = new Schema({
103
+ * name: { type: 'string' },
104
+ * address: { $ref: 'address-schema' }
105
+ * }, 'user-schema');
106
+ *
107
+ * const user = {
108
+ * name: 'John',
109
+ * address: {
110
+ * street: '123 Main St',
111
+ * city: 'New York'
112
+ * }
113
+ * };
114
+ *
115
+ * const schemasMap = {
116
+ * 'address-schema': addressSchema.jsonSchema
117
+ * };
118
+ *
119
+ * mapObjectProperties(user, userSchema.jsonSchema, schemasMap, (propName) => {
120
+ * console.log(`Processing: ${propName}`);
121
+ * });
122
+ * // Output:
123
+ * // Processing: name
124
+ * // Processing: address
125
+ * // Processing: street
126
+ * // Processing: city
127
+ *
128
+ * @example
129
+ * // Example 4: Process arrays with object items
130
+ * const schema = new Schema({
131
+ * tags: {
132
+ * type: 'array',
133
+ * items: {
134
+ * type: 'object',
135
+ * properties: {
136
+ * name: { type: 'string' },
137
+ * value: { type: 'string' }
138
+ * }
139
+ * }
140
+ * }
141
+ * }, 'item-schema');
142
+ *
143
+ * const item = {
144
+ * tags: [
145
+ * { name: 'tag1', value: 'value1' },
146
+ * { name: 'tag2', value: 'value2' }
147
+ * ]
148
+ * };
149
+ *
150
+ * mapObjectProperties(item, schema.jsonSchema, {}, (propName, propSchema, obj) => {
151
+ * if (propSchema.type === 'string') {
152
+ * obj[propName] = String(obj[propName]).toUpperCase();
153
+ * }
154
+ * });
155
+ * // Result: tags array items have uppercase name and value properties
156
+ *
157
+ * @param object - The target object to traverse
158
+ * @param jsonSchema - The JSON schema defining the object structure
159
+ * @param schemasMap - Map of schema IDs to schema objects for resolving $ref references
160
+ * @param callback - Function called for each property with (propertyName, propertySchema, object)
161
+ */
162
+ const mapObjectProperties = (
163
+ object: TargetObject,
164
+ jsonSchema: JsonSchema,
165
+ schemasMap: JsonSchemasMap,
166
+ callback: (propertyName: string, propertySchema: PropertySchema, object: TargetObject) => void
167
+ ) => {
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
+
178
+ const hasProperties = !!objectSchema.properties;
179
+
180
+ // Guard against malformed schemas without properties
181
+ if (!hasProperties) {
182
+ return;
183
+ }
184
+
185
+ const { properties: objectProperties } = objectSchema;
186
+
187
+ for (const propertyName in objectProperties) {
188
+ const property = objectProperties[propertyName];
189
+
190
+ callback(propertyName, property, object);
191
+
192
+ const value = object[propertyName];
193
+ const isValueUndefined = isUndefined(value);
194
+
195
+ if (isValueUndefined) {
196
+ continue;
197
+ }
198
+
199
+ const { $ref: refSchemaId } = property as ReferencePropertySchema;
200
+
201
+ const isReference = !isUndefined(refSchemaId);
202
+
203
+ if (isReference) {
204
+ const referenceSchema = got(schemasMap, refSchemaId, 'Schema "$PATH" not found');
205
+
206
+ const isObjectValue = value && typeof value === 'object' && !Array.isArray(value);
207
+
208
+ // Only recursively process if the value is an object (not null, undefined, or primitive)
209
+ if (isObjectValue) {
210
+ mapObjectProperties(value as TargetObject, referenceSchema, schemasMap, callback);
211
+ }
212
+ continue;
213
+ }
214
+
215
+ const { type } = property as ObjectPropertySchema | ArrayPropertySchema;
216
+
217
+ const isObject = type === 'object';
218
+
219
+ if (isObject) {
220
+ const { properties = {} } = property as ObjectPropertySchema;
221
+
222
+ const isObjectValue = value && typeof value === 'object' && !Array.isArray(value);
223
+
224
+ // Only recursively process if the value is an object (not null, undefined, or primitive)
225
+ if (isObjectValue) {
226
+ const nestedJsonSchema = {
227
+ id: `${objectSchema.id}.${propertyName}.properties`,
228
+ properties
229
+ };
230
+
231
+ mapObjectProperties(value as TargetObject, nestedJsonSchema, schemasMap, callback);
232
+ }
233
+ continue;
234
+ }
235
+
236
+ const isArray = type === 'array';
237
+
238
+ if (isArray) {
239
+ const { items } = property as ArrayPropertySchema;
240
+
241
+ const hasItems = !!items;
242
+ const isArrayValue = Array.isArray(value);
243
+
244
+ // Only process if value is an array and items schema is defined
245
+ if (isArrayValue && hasItems) {
246
+ const { $ref: itemRefSchemaId } = items as ReferencePropertySchema;
247
+
248
+ const { properties: itemObjectProperties = {} } = items as ObjectPropertySchema;
249
+
250
+ const isItemReference = !isUndefined(itemRefSchemaId);
251
+
252
+ const itemSchema = isItemReference
253
+ ? got(schemasMap, itemRefSchemaId, 'Schema "$PATH" not found')
254
+ : {
255
+ id: `${objectSchema.id}.${propertyName}.items.properties`,
256
+ properties: itemObjectProperties
257
+ };
258
+
259
+ for (const valueItem of value) {
260
+ const isObjectItem = valueItem && typeof valueItem === 'object' && !Array.isArray(valueItem);
261
+
262
+ // Only recursively process if the item is an object (not null, undefined, or primitive)
263
+ if (isObjectItem) {
264
+ mapObjectProperties(valueItem as TargetObject, itemSchema, schemasMap, callback);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ };
271
+
272
+ export default mapObjectProperties;
@@ -0,0 +1,247 @@
1
+ import { get, isUndefined } from 'lodash';
2
+
3
+ import normalizeType from './normalizeType';
4
+ import mapObjectProperties from './mapObjectProperties';
5
+ import type { TargetObject, JsonSchema, JsonSchemasMap, PropertySchema } from './JsonSchema';
6
+
7
+ /**
8
+ * Normalizes object attribute values based on a JSON Schema definition.
9
+ *
10
+ * ## Intent
11
+ *
12
+ * This function ensures that object properties conform to their schema definitions by:
13
+ * 1. Setting default values for properties that are undefined (but not null)
14
+ * 2. Normalizing existing values to match their schema-defined types (e.g., converting
15
+ * string "123" to number 123, or string "true" to boolean true)
16
+ *
17
+ * The function operates recursively, processing nested objects, arrays, and referenced
18
+ * schemas ($ref) to ensure all properties throughout the object tree are normalized
19
+ * according to their respective schema definitions.
20
+ *
21
+ * This is particularly useful in data processing pipelines where data may come from
22
+ * external sources (forms, APIs, databases) with inconsistent types, but needs to be
23
+ * normalized before validation or further processing.
24
+ *
25
+ * ## Use Cases
26
+ *
27
+ * 1. **Form data processing**: HTML forms submit all values as strings. This function
28
+ * converts them to their expected types (numbers, booleans) based on schema definitions
29
+ * and fills in default values for missing fields.
30
+ *
31
+ * 2. **API response normalization**: When consuming APIs that return loosely-typed data
32
+ * (e.g., numbers as strings, booleans as strings), this function ensures values match
33
+ * the expected schema types before validation or business logic processing.
34
+ *
35
+ * 3. **Configuration object initialization**: Setting default values and normalizing types
36
+ * for configuration objects based on their schema definitions, ensuring consistent
37
+ * structure and types throughout the application.
38
+ *
39
+ * 4. **Data migration and transformation**: Normalizing data structures during migration
40
+ * or transformation processes where source data may have inconsistent types but target
41
+ * schema requires specific types.
42
+ *
43
+ * 5. **Pre-validation normalization**: Preparing objects for schema validation by ensuring
44
+ * types are correct and defaults are applied, reducing validation errors and improving
45
+ * data quality.
46
+ *
47
+ * ## Behavior
48
+ *
49
+ * - **Default values**: Properties that are `undefined` will be set to their schema-defined
50
+ * default value (if one exists). Properties that are `null` are left as `null` and will
51
+ * not receive default values. Default values are also normalized according to their type
52
+ * (e.g., a default string "123" with type "number" will be converted to the number 123).
53
+ *
54
+ * - **Type normalization**: Properties with existing values (including default values that
55
+ * were just set) are normalized to match their schema type using `normalizeType`. This
56
+ * includes converting strings to numbers/booleans where appropriate, while preserving the
57
+ * original value if conversion is not possible.
58
+ *
59
+ * - **Recursive processing**: The function processes nested objects, arrays, and schema
60
+ * references ($ref) recursively, ensuring all nested properties are normalized.
61
+ *
62
+ * - **Non-destructive**: The function mutates the input object in place. If you need to
63
+ * preserve the original, create a deep copy before calling this function.
64
+ *
65
+ * ## Examples
66
+ *
67
+ * ### Basic Usage: Default Values and Type Normalization
68
+ * ```typescript
69
+ * import Schema from './Schema';
70
+ * import normalizeAttributes from './normalizeAttributes';
71
+ *
72
+ * const schema = new Schema({
73
+ * name: { type: 'string', default: 'Anonymous' },
74
+ * age: { type: 'number' },
75
+ * isActive: { type: 'boolean', default: false }
76
+ * }, 'user-schema');
77
+ *
78
+ * const user = {
79
+ * age: '25' // string that should be a number
80
+ * };
81
+ *
82
+ * normalizeAttributes(user, schema.jsonSchema, {});
83
+ *
84
+ * // Result:
85
+ * // {
86
+ * // name: 'Anonymous', // default value applied
87
+ * // age: 25, // string converted to number
88
+ * // isActive: false // default value applied
89
+ * // }
90
+ * ```
91
+ *
92
+ * ### Nested Objects
93
+ * ```typescript
94
+ * const schema = new Schema({
95
+ * address: {
96
+ * type: 'object',
97
+ * properties: {
98
+ * street: { type: 'string', default: 'Unknown' },
99
+ * zipCode: { type: 'number' }
100
+ * }
101
+ * }
102
+ * }, 'profile-schema');
103
+ *
104
+ * const profile = {
105
+ * address: {
106
+ * zipCode: '12345' // string that should be a number
107
+ * }
108
+ * };
109
+ *
110
+ * normalizeAttributes(profile, schema.jsonSchema, {});
111
+ *
112
+ * // Result:
113
+ * // {
114
+ * // address: {
115
+ * // street: 'Unknown', // default value applied
116
+ * // zipCode: 12345 // string converted to number
117
+ * // }
118
+ * // }
119
+ * ```
120
+ *
121
+ * ### Arrays with Schema References
122
+ * ```typescript
123
+ * const itemSchema = new Schema({
124
+ * id: { type: 'number' },
125
+ * name: { type: 'string', default: 'Unnamed' }
126
+ * }, 'item-schema');
127
+ *
128
+ * const schema = new Schema({
129
+ * items: {
130
+ * type: 'array',
131
+ * items: { $ref: 'item-schema' }
132
+ * }
133
+ * }, 'collection-schema');
134
+ *
135
+ * const collection = {
136
+ * items: [
137
+ * { id: '1' }, // id is a string, should be number
138
+ * { id: '2', name: 'Item 2' }
139
+ * ]
140
+ * };
141
+ *
142
+ * const schemasMap = {
143
+ * 'item-schema': itemSchema.jsonSchema
144
+ * };
145
+ *
146
+ * normalizeAttributes(collection, schema.jsonSchema, schemasMap);
147
+ *
148
+ * // Result:
149
+ * // {
150
+ * // items: [
151
+ * // { id: 1, name: 'Unnamed' }, // id normalized, default name applied
152
+ * // { id: 2, name: 'Item 2' } // id normalized, existing name preserved
153
+ * // ]
154
+ * // }
155
+ * ```
156
+ *
157
+ * ### Boolean Normalization
158
+ * ```typescript
159
+ * const schema = new Schema({
160
+ * enabled: { type: 'boolean', default: false },
161
+ * verified: { type: 'boolean' }
162
+ * }, 'settings-schema');
163
+ *
164
+ * const settings = {
165
+ * verified: 'yes' // string that should be boolean
166
+ * };
167
+ *
168
+ * normalizeAttributes(settings, schema.jsonSchema, {});
169
+ *
170
+ * // Result:
171
+ * // {
172
+ * // enabled: false, // default value applied
173
+ * // verified: true // string "yes" converted to boolean true
174
+ * // }
175
+ * ```
176
+ *
177
+ * ### Handling Null Values
178
+ * ```typescript
179
+ * const schema = new Schema({
180
+ * optionalField: { type: 'string', default: 'default-value' }
181
+ * }, 'test-schema');
182
+ *
183
+ * const obj1 = {}; // undefined → default applied
184
+ * const obj2 = { optionalField: null }; // null → no default applied
185
+ *
186
+ * normalizeAttributes(obj1, schema.jsonSchema, {});
187
+ * normalizeAttributes(obj2, schema.jsonSchema, {});
188
+ *
189
+ * // obj1: { optionalField: 'default-value' }
190
+ * // obj2: { optionalField: null } // null preserved, default not applied
191
+ * ```
192
+ *
193
+ * ### Default Value Normalization
194
+ * ```typescript
195
+ * const schema = new Schema({
196
+ * count: { type: 'number', default: '42' }, // default is string, type is number
197
+ * enabled: { type: 'boolean', default: 'true' } // default is string, type is boolean
198
+ * }, 'config-schema');
199
+ *
200
+ * const config = {};
201
+ *
202
+ * normalizeAttributes(config, schema.jsonSchema, {});
203
+ *
204
+ * // Result:
205
+ * // {
206
+ * // count: 42, // default string "42" normalized to number
207
+ * // enabled: true // default string "true" normalized to boolean
208
+ * // }
209
+ * ```
210
+ *
211
+ * @param object - The target object to normalize (mutated in place)
212
+ * @param jsonSchema - The JSON Schema definition describing the object structure
213
+ * @param jsonSchemasMap - Map of schema IDs to schema definitions, used for resolving $ref references
214
+ * @returns void (mutates the input object)
215
+ */
216
+ const normalizeAttributes = (object: TargetObject, jsonSchema: JsonSchema, jsonSchemasMap: JsonSchemasMap) => {
217
+ /** Callback to normalize value based on property type defined in schema */
218
+ const callback = (propertyName: string, propertySchema: PropertySchema, object: TargetObject) => {
219
+ let value = object[propertyName];
220
+
221
+ const type = get(propertySchema, 'type');
222
+ const defaultValue = get(propertySchema, 'default');
223
+
224
+ const hasValue = !isUndefined(value);
225
+ const hasDefaultValue = !isUndefined(defaultValue);
226
+ const shouldSetDefaultValue = hasDefaultValue && !hasValue;
227
+
228
+ // Set default value if property is undefined and default exists
229
+ if (shouldSetDefaultValue) {
230
+ object[propertyName] = defaultValue;
231
+ value = defaultValue; // Update value reference for normalization
232
+ }
233
+
234
+ const hasType = !!type;
235
+ const hasValueAfterDefault = !isUndefined(value);
236
+ const shouldNormalizeValue = hasType && hasValueAfterDefault;
237
+
238
+ // Normalize the current value (original or default) if type is defined
239
+ if (shouldNormalizeValue) {
240
+ object[propertyName] = normalizeType(type, value);
241
+ }
242
+ };
243
+
244
+ mapObjectProperties(object, jsonSchema, jsonSchemasMap, callback);
245
+ };
246
+
247
+ export default normalizeAttributes;