@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.
- 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/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 +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -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 +28 -21
- 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__/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/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 +13 -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,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;
|