@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,273 @@
1
+ import { isUndefined, uniq } from 'lodash';
2
+
3
+ import Schema from '../Schema';
4
+ import got from './got';
5
+ import {
6
+ EnumSchema,
7
+ ObjectSchema,
8
+ ArrayPropertySchema,
9
+ ObjectPropertySchema,
10
+ ReferencePropertySchema,
11
+ } from './JsonSchema';
12
+
13
+ /**
14
+ * Recursively extracts all referenced schema IDs from a schema structure.
15
+ *
16
+ * **Intent:** Traverse a schema's entire structure (including nested objects and arrays)
17
+ * to collect all schema IDs that are referenced via `$ref` properties. This enables
18
+ * dependency resolution, schema bundling, and validation of schema completeness.
19
+ *
20
+ * **Use Cases:**
21
+ * - **Dependency Resolution:** Identify all schemas that a given schema depends on,
22
+ * ensuring they are loaded before validation
23
+ * - **Schema Bundling:** Collect all related schemas into a single bundle for
24
+ * distribution or storage
25
+ * - **Validation Preparation:** Pre-load all referenced schemas to ensure complete
26
+ * validation context
27
+ * - **Dependency Graph Building:** Understand the relationships and dependencies
28
+ * between schemas in a schema registry
29
+ * - **Schema Analysis:** Analyze schema complexity by identifying all dependencies
30
+ * - **Circular Reference Detection:** (Note: current implementation does not handle
31
+ * circular references and will recurse infinitely)
32
+ *
33
+ * **Behavior:**
34
+ * - Returns an empty array for enum schemas (they don't reference other schemas)
35
+ * - Recursively traverses nested object properties
36
+ * - Handles array items that reference schemas or contain object properties
37
+ * - Follows nested references to collect transitive dependencies
38
+ * - Returns unique schema IDs (deduplicates if same schema is referenced multiple times)
39
+ * - Throws an error if a referenced schema is not found in the schemasMap
40
+ *
41
+ * **Example - Simple Reference:**
42
+ * ```typescript
43
+ * const userSchema = new Schema({
44
+ * profile: { $ref: 'Profile' }
45
+ * }, 'User');
46
+ *
47
+ * const profileSchema = new Schema({
48
+ * name: { type: 'string' }
49
+ * }, 'Profile');
50
+ *
51
+ * const schemasMap = { 'Profile': profileSchema };
52
+ * const referenceIds = getReferenceIds(userSchema, schemasMap);
53
+ * // Returns: ['Profile']
54
+ * ```
55
+ *
56
+ * **Example - Multiple References:**
57
+ * ```typescript
58
+ * const orderSchema = new Schema({
59
+ * customer: { $ref: 'Customer' },
60
+ * product: { $ref: 'Product' },
61
+ * shipping: { $ref: 'Address' }
62
+ * }, 'Order');
63
+ *
64
+ * const schemasMap = {
65
+ * 'Customer': customerSchema,
66
+ * 'Product': productSchema,
67
+ * 'Address': addressSchema
68
+ * };
69
+ * const referenceIds = getReferenceIds(orderSchema, schemasMap);
70
+ * // Returns: ['Customer', 'Product', 'Address']
71
+ * ```
72
+ *
73
+ * **Example - Nested References:**
74
+ * ```typescript
75
+ * const userSchema = new Schema({
76
+ * profile: { $ref: 'Profile' }
77
+ * }, 'User');
78
+ *
79
+ * const profileSchema = new Schema({
80
+ * address: { $ref: 'Address' }
81
+ * }, 'Profile');
82
+ *
83
+ * const addressSchema = new Schema({
84
+ * street: { type: 'string' }
85
+ * }, 'Address');
86
+ *
87
+ * const schemasMap = {
88
+ * 'Profile': profileSchema,
89
+ * 'Address': addressSchema
90
+ * };
91
+ * const referenceIds = getReferenceIds(userSchema, schemasMap);
92
+ * // Returns: ['Profile', 'Address'] (includes transitive dependencies)
93
+ * ```
94
+ *
95
+ * **Example - Array with Reference Items:**
96
+ * ```typescript
97
+ * const orderSchema = new Schema({
98
+ * items: {
99
+ * type: 'array',
100
+ * items: { $ref: 'OrderItem' }
101
+ * }
102
+ * }, 'Order');
103
+ *
104
+ * const schemasMap = { 'OrderItem': orderItemSchema };
105
+ * const referenceIds = getReferenceIds(orderSchema, schemasMap);
106
+ * // Returns: ['OrderItem']
107
+ * ```
108
+ *
109
+ * **Example - Nested Object Properties:**
110
+ * ```typescript
111
+ * const userSchema = new Schema({
112
+ * contact: {
113
+ * type: 'object',
114
+ * properties: {
115
+ * address: { $ref: 'Address' }
116
+ * }
117
+ * }
118
+ * }, 'User');
119
+ *
120
+ * const schemasMap = { 'Address': addressSchema };
121
+ * const referenceIds = getReferenceIds(userSchema, schemasMap);
122
+ * // Returns: ['Address']
123
+ * ```
124
+ *
125
+ * **Example - Complex Mixed Structure:**
126
+ * ```typescript
127
+ * const orderSchema = new Schema({
128
+ * customer: { $ref: 'Customer' },
129
+ * items: {
130
+ * type: 'array',
131
+ * items: {
132
+ * type: 'object',
133
+ * properties: {
134
+ * product: { $ref: 'Product' }
135
+ * }
136
+ * }
137
+ * },
138
+ * shipping: {
139
+ * type: 'object',
140
+ * properties: {
141
+ * address: { $ref: 'Address' }
142
+ * }
143
+ * }
144
+ * }, 'Order');
145
+ *
146
+ * const schemasMap = {
147
+ * 'Customer': customerSchema,
148
+ * 'Product': productSchema,
149
+ * 'Address': addressSchema
150
+ * };
151
+ * const referenceIds = getReferenceIds(orderSchema, schemasMap);
152
+ * // Returns: ['Customer', 'Product', 'Address']
153
+ * ```
154
+ *
155
+ * **Example - Duplicate References:**
156
+ * ```typescript
157
+ * const schema = new Schema({
158
+ * field1: { $ref: 'SharedSchema' },
159
+ * field2: { $ref: 'SharedSchema' }
160
+ * }, 'Test');
161
+ *
162
+ * const schemasMap = { 'SharedSchema': sharedSchema };
163
+ * const referenceIds = getReferenceIds(schema, schemasMap);
164
+ * // Returns: ['SharedSchema'] (deduplicated)
165
+ * ```
166
+ *
167
+ * @param schema - The schema to extract references from
168
+ * @param schemasMap - A map of schema IDs to Schema instances, used to resolve
169
+ * referenced schemas and traverse nested references
170
+ * @returns An array of unique schema IDs that are referenced (directly or indirectly)
171
+ * by the given schema
172
+ * @throws Error if a referenced schema is not found in the schemasMap
173
+ *
174
+ * **Limitations:**
175
+ * - Does not handle circular references (will cause infinite recursion)
176
+ * - Requires all referenced schemas to be present in schemasMap
177
+ */
178
+ const getReferenceIds = (schema: Schema, schemasMap: Record<string, Schema>): string[] => {
179
+ /** Returns schema from the map by ID */
180
+ const getSchema = (id: string) => got(schemasMap, id, 'Schema "$PATH" not found');
181
+
182
+ let referenceIds: string[] = [];
183
+
184
+ const { jsonSchema } = schema;
185
+ const { enum: isEnum } = (jsonSchema as EnumSchema);
186
+
187
+ if (isEnum) {
188
+ return [];
189
+ }
190
+
191
+ const objectSchema = (jsonSchema as ObjectSchema);
192
+
193
+ for (const propertyName in objectSchema.properties) {
194
+ const property = objectSchema.properties[propertyName];
195
+
196
+ const { $ref: refSchemaId } = (property as ReferencePropertySchema);
197
+
198
+ const isReference = !isUndefined(refSchemaId);
199
+
200
+ if (isReference) {
201
+ const refJsonSchema = getSchema(refSchemaId);
202
+ const nestedReferenceIds = getReferenceIds(refJsonSchema, schemasMap);
203
+
204
+ referenceIds = [
205
+ refSchemaId,
206
+ ...referenceIds,
207
+ ...nestedReferenceIds
208
+ ];
209
+
210
+ continue;
211
+ }
212
+
213
+ const { type } = (property as ArrayPropertySchema | ObjectPropertySchema);
214
+
215
+ const isObject = type === 'object';
216
+
217
+ if (isObject) {
218
+ // istanbul ignore next - unreachable defensive code: properties is always set by normalizeProperties in Schema constructor
219
+ const { properties = {} } = (property as ObjectPropertySchema);
220
+
221
+ const nestedSchema = new Schema(properties, `${objectSchema.id}.${propertyName}.properties`);
222
+ const nestedReferenceIds = getReferenceIds(nestedSchema, schemasMap);
223
+
224
+ referenceIds = [
225
+ ...referenceIds,
226
+ ...nestedReferenceIds
227
+ ];
228
+
229
+ continue;
230
+ }
231
+
232
+ const isArray = type === 'array';
233
+
234
+ if (!isArray) {
235
+ continue;
236
+ }
237
+
238
+ const { items } = (property as ArrayPropertySchema);
239
+
240
+ const itemRefSchemaId = (items as ReferencePropertySchema).$ref;
241
+
242
+ if (itemRefSchemaId) {
243
+ const itemJsonSchema = getSchema(itemRefSchemaId);
244
+ const nestedReferenceIds = getReferenceIds(itemJsonSchema, schemasMap);
245
+
246
+ referenceIds = [
247
+ itemRefSchemaId,
248
+ ...referenceIds,
249
+ ...nestedReferenceIds
250
+ ];
251
+
252
+ continue;
253
+ }
254
+
255
+ const itemProperties = (items as ObjectPropertySchema).properties;
256
+
257
+ if (itemProperties) {
258
+ const itemSchema = new Schema(itemProperties, `${objectSchema.id}.${propertyName}.items.properties`);
259
+ const itemReferenceIds = getReferenceIds(itemSchema, schemasMap);
260
+
261
+ referenceIds = [
262
+ ...referenceIds,
263
+ ...itemReferenceIds
264
+ ];
265
+
266
+ continue;
267
+ }
268
+ }
269
+
270
+ return uniq(referenceIds);
271
+ };
272
+
273
+ export default getReferenceIds;
@@ -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;