@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.
- package/README.md +19 -14
- package/dist/CredentialFactory.d.ts +345 -0
- package/dist/CredentialFactory.d.ts.map +1 -0
- package/dist/CredentialFactory.js +381 -0
- package/dist/CredentialFactory.js.map +1 -0
- package/dist/Schema.d.ts +448 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +506 -0
- package/dist/Schema.js.map +1 -0
- package/dist/ValidationError.d.ts +70 -0
- package/dist/ValidationError.d.ts.map +1 -0
- package/dist/ValidationError.js +78 -0
- package/dist/ValidationError.js.map +1 -0
- package/dist/Validator.d.ts +483 -0
- package/dist/Validator.d.ts.map +1 -0
- package/dist/Validator.js +570 -0
- package/dist/Validator.js.map +1 -0
- package/dist/helpers/JsonSchema.d.ts +99 -0
- package/dist/helpers/JsonSchema.d.ts.map +1 -0
- package/dist/helpers/JsonSchema.js +3 -0
- package/dist/helpers/JsonSchema.js.map +1 -0
- package/dist/helpers/cleanupAttributes.d.ts +34 -0
- package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
- package/dist/helpers/cleanupAttributes.js +113 -0
- package/dist/helpers/cleanupAttributes.js.map +1 -0
- package/dist/helpers/cleanupNulls.d.ts +27 -0
- package/dist/helpers/cleanupNulls.d.ts.map +1 -0
- package/dist/helpers/cleanupNulls.js +96 -0
- package/dist/helpers/cleanupNulls.js.map +1 -0
- package/dist/helpers/createSchemasMap.d.ts +67 -0
- package/dist/helpers/createSchemasMap.d.ts.map +1 -0
- package/dist/helpers/createSchemasMap.js +200 -0
- package/dist/helpers/createSchemasMap.js.map +1 -0
- package/dist/helpers/getReferenceIds.d.ts +169 -0
- package/dist/helpers/getReferenceIds.d.ts.map +1 -0
- package/dist/helpers/getReferenceIds.js +241 -0
- package/dist/helpers/getReferenceIds.js.map +1 -0
- package/dist/helpers/got.d.ts +60 -0
- package/dist/helpers/got.d.ts.map +1 -0
- package/dist/helpers/got.js +72 -0
- package/dist/helpers/got.js.map +1 -0
- package/dist/helpers/mapObjectProperties.d.ts +150 -0
- package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
- package/dist/helpers/mapObjectProperties.js +229 -0
- package/dist/helpers/mapObjectProperties.js.map +1 -0
- package/dist/helpers/normalizeAttributes.d.ts +213 -0
- package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
- package/dist/helpers/normalizeAttributes.js +243 -0
- package/dist/helpers/normalizeAttributes.js.map +1 -0
- package/dist/helpers/normalizeProperties.d.ts +168 -0
- package/dist/helpers/normalizeProperties.d.ts.map +1 -0
- package/dist/helpers/normalizeProperties.js +223 -0
- package/dist/helpers/normalizeProperties.js.map +1 -0
- package/dist/helpers/normalizeRequired.d.ts +159 -0
- package/dist/helpers/normalizeRequired.d.ts.map +1 -0
- package/dist/helpers/normalizeRequired.js +206 -0
- package/dist/helpers/normalizeRequired.js.map +1 -0
- package/dist/helpers/normalizeType.d.ts +81 -0
- package/dist/helpers/normalizeType.d.ts.map +1 -0
- package/dist/helpers/normalizeType.js +210 -0
- package/dist/helpers/normalizeType.js.map +1 -0
- package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
- package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
- package/dist/helpers/nullifyEmptyValues.js +191 -0
- package/dist/helpers/nullifyEmptyValues.js.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.js +138 -0
- package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
- package/dist/helpers/validateId.d.ts +39 -0
- package/dist/helpers/validateId.d.ts.map +1 -0
- package/dist/helpers/validateId.js +51 -0
- package/dist/helpers/validateId.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/ld/documentLoader.d.ts +8 -0
- package/dist/ld/documentLoader.d.ts.map +1 -0
- package/dist/ld/documentLoader.js +24 -0
- package/dist/ld/documentLoader.js.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.js +32 -0
- package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
- package/dist/ld/getLinkedDataContext.d.ts +19 -0
- package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
- package/dist/ld/getLinkedDataContext.js +50 -0
- package/dist/ld/getLinkedDataContext.js.map +1 -0
- package/eslint.config.mjs +32 -52
- package/examples/credentials/createAccountCredential.ts +27 -0
- package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
- package/examples/index.ts +7 -0
- package/examples/schemas/FavoriteItemSchema.ts +27 -0
- package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
- package/examples/schemas/PreferencesSchema.ts +29 -0
- package/examples/schemas/ProfileSchema.ts +91 -0
- package/examples/schemas/Status.yaml +3 -0
- package/examples/schemas/StatusSchema.ts +12 -0
- package/jest.config.mjs +5 -0
- package/package.json +27 -20
- package/src/CredentialFactory.ts +392 -0
- package/src/Schema.ts +583 -0
- package/src/ValidationError.ts +90 -0
- package/src/Validator.ts +603 -0
- package/src/__tests__/CredentialFactory.test.ts +588 -0
- package/src/__tests__/Schema.test.ts +371 -0
- package/src/__tests__/ValidationError.test.ts +235 -0
- package/src/__tests__/Validator.test.ts +787 -0
- package/src/helpers/JsonSchema.ts +119 -0
- package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
- package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
- package/src/helpers/__tests__/createSchemasMap.test.ts +238 -0
- package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
- package/src/helpers/__tests__/got.test.ts +193 -0
- package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
- package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
- package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
- package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
- package/src/helpers/__tests__/normalizeType.test.ts +772 -0
- package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
- package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
- package/src/helpers/__tests__/validateId.test.ts +118 -0
- package/src/helpers/cleanupAttributes.ts +151 -0
- package/src/helpers/cleanupNulls.ts +106 -0
- package/src/helpers/createSchemasMap.ts +212 -0
- package/src/helpers/getReferenceIds.ts +273 -0
- package/src/helpers/got.ts +73 -0
- package/src/helpers/mapObjectProperties.ts +272 -0
- package/src/helpers/normalizeAttributes.ts +247 -0
- package/src/helpers/normalizeProperties.ts +249 -0
- package/src/helpers/normalizeRequired.ts +233 -0
- package/src/helpers/normalizeType.ts +235 -0
- package/src/helpers/nullifyEmptyValues.ts +207 -0
- package/src/helpers/removeRequiredAndDefault.ts +151 -0
- package/src/helpers/validateId.ts +53 -0
- package/src/index.ts +17 -0
- package/src/ld/__tests__/documentLoader.test.ts +57 -0
- package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
- package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
- package/src/ld/documentLoader.ts +28 -0
- package/src/ld/getLinkedDataAttributeType.ts +46 -0
- package/src/ld/getLinkedDataContext.ts +80 -0
- package/tsconfig.json +27 -0
- package/types/credentials-context.d.ts +14 -0
- package/types/security-context.d.ts +6 -0
- package/examples/Status.yaml +0 -3
- package/examples/createAccountCredential.js +0 -27
- package/examples/createMineSweeperScoreCredential.js +0 -63
- package/examples/index.js +0 -9
- package/src/CredentialFactory.js +0 -67
- package/src/CredentialFactory.spec.js +0 -131
- package/src/Schema.js +0 -104
- package/src/Schema.spec.js +0 -172
- package/src/ValidationError.js +0 -31
- package/src/Validator.js +0 -128
- package/src/Validator.spec.js +0 -355
- package/src/helpers/cleanupAttributes.js +0 -71
- package/src/helpers/cleanupNulls.js +0 -42
- package/src/helpers/getReferenceIds.js +0 -71
- package/src/helpers/mapObject.js +0 -65
- package/src/helpers/normalizeAttributes.js +0 -28
- package/src/helpers/normalizeProperties.js +0 -61
- package/src/helpers/normalizeRequired.js +0 -37
- package/src/helpers/normalizeType.js +0 -41
- package/src/helpers/nullifyEmptyValues.js +0 -57
- package/src/helpers/removeRequiredAndDefault.js +0 -30
- package/src/helpers/validateId.js +0 -19
- package/src/index.d.ts +0 -25
- package/src/index.js +0 -8
- package/src/ld/documentLoader.js +0 -25
- package/src/ld/documentLoader.spec.js +0 -12
- package/src/ld/getLinkedDataContext.js +0 -63
- package/src/ld/getLinkedDataType.js +0 -38
- /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
- /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;
|