@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,118 @@
|
|
|
1
|
+
import validateId from '../validateId';
|
|
2
|
+
|
|
3
|
+
const name = 'testId';
|
|
4
|
+
|
|
5
|
+
describe('validateId(name, value)', () => {
|
|
6
|
+
describe('valid inputs', () => {
|
|
7
|
+
describe('URLs', () => {
|
|
8
|
+
it('should accept HTTP URLs', () => {
|
|
9
|
+
expect(() => validateId(name, 'http://example.com')).not.toThrow();
|
|
10
|
+
expect(() => validateId(name, 'http://example.com/path')).not.toThrow();
|
|
11
|
+
expect(() => validateId(name, 'http://example.com:8080/path?query=value')).not.toThrow();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should accept HTTPS URLs', () => {
|
|
15
|
+
expect(() => validateId(name, 'https://example.com')).not.toThrow();
|
|
16
|
+
expect(() => validateId(name, 'https://example.com/path')).not.toThrow();
|
|
17
|
+
expect(() => validateId(name, 'https://example.com:443/path#fragment')).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should accept other URL schemes', () => {
|
|
21
|
+
expect(() => validateId(name, 'ftp://example.com')).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should accept URLs without protocol', () => {
|
|
25
|
+
expect(() => validateId(name, 'example.com')).not.toThrow();
|
|
26
|
+
expect(() => validateId(name, 'subdomain.example.com')).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should accept IP address URLs', () => {
|
|
30
|
+
expect(() => validateId(name, 'http://192.168.1.1')).not.toThrow();
|
|
31
|
+
expect(() => validateId(name, 'https://8.8.8.8')).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should accept URLs with special characters', () => {
|
|
35
|
+
expect(() => validateId(name, 'https://example.com/path%20with%20spaces')).not.toThrow();
|
|
36
|
+
expect(() => validateId(name, 'https://example.com/path?key=value&other=123')).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('DIDs', () => {
|
|
41
|
+
it('should accept valid DIDs', () => {
|
|
42
|
+
expect(() => validateId(name, 'did:example:123456789')).not.toThrow();
|
|
43
|
+
expect(() => validateId(name, 'did:web:example.com')).not.toThrow();
|
|
44
|
+
expect(() => validateId(name, 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK')).not.toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should accept DIDs case-insensitively', () => {
|
|
48
|
+
expect(() => validateId(name, 'DID:example:123456789')).not.toThrow();
|
|
49
|
+
expect(() => validateId(name, 'Did:example:123456789')).not.toThrow();
|
|
50
|
+
expect(() => validateId(name, 'dId:example:123456789')).not.toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should accept DIDs with various methods', () => {
|
|
54
|
+
expect(() => validateId(name, 'did:ethr:0x1234567890abcdef')).not.toThrow();
|
|
55
|
+
expect(() => validateId(name, 'did:ion:EiClkZMDZhPKV7e3j4wUmxLukn6YxvfN8v2F2b3X5r8tQ')).not.toThrow();
|
|
56
|
+
expect(() => validateId(name, 'did:peer:123456789')).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should accept bare did: prefix (no method or id)', () => {
|
|
60
|
+
expect(() => validateId(name, 'did:')).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('invalid inputs', () => {
|
|
66
|
+
describe('missing value', () => {
|
|
67
|
+
it('should throw when value is empty string', () => {
|
|
68
|
+
expect(() => validateId(name, '')).toThrow('Parameter "testId" is required');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw when value is null', () => {
|
|
72
|
+
expect(() => validateId(name, null)).toThrow('Parameter "testId" is required');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should throw when value is undefined', () => {
|
|
76
|
+
expect(() => validateId(name, undefined)).toThrow('Parameter "testId" is required');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('malformed value', () => {
|
|
81
|
+
it('should throw when value is not a URL or DID', () => {
|
|
82
|
+
expect(() => validateId(name, 'not-a-url-or-did')).toThrow(
|
|
83
|
+
'Parameter "testId" must be a URL, received: "not-a-url-or-did"'
|
|
84
|
+
);
|
|
85
|
+
expect(() => validateId(name, 'just some text')).toThrow(
|
|
86
|
+
'Parameter "testId" must be a URL, received: "just some text"'
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should throw when value is incomplete DID prefix without colon or method', () => {
|
|
91
|
+
expect(() => validateId(name, 'did')).toThrow(
|
|
92
|
+
'Parameter "testId" must be a URL, received: "did"'
|
|
93
|
+
);
|
|
94
|
+
expect(() => validateId(name, 'did ')).toThrow(
|
|
95
|
+
'Parameter "testId" must be a URL, received: "did "'
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should throw when value is whitespace-only', () => {
|
|
100
|
+
expect(() => validateId(name, ' ')).toThrow(
|
|
101
|
+
'Parameter "testId" must be a URL, received: " "'
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('error messages', () => {
|
|
108
|
+
it('should include parameter name in required error', () => {
|
|
109
|
+
expect(() => validateId('schemaId', '')).toThrow('Parameter "schemaId" is required');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should include parameter name in invalid-format error', () => {
|
|
113
|
+
expect(() => validateId('myParam', 'invalid')).toThrow(
|
|
114
|
+
'Parameter "myParam" must be a URL, received: "invalid"'
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { isUndefined } from 'lodash';
|
|
2
|
+
|
|
3
|
+
import got from './got';
|
|
4
|
+
import type {
|
|
5
|
+
JsonSchema,
|
|
6
|
+
EnumSchema,
|
|
7
|
+
TargetObject,
|
|
8
|
+
ObjectSchema,
|
|
9
|
+
JsonSchemasMap,
|
|
10
|
+
ArrayPropertySchema,
|
|
11
|
+
ObjectPropertySchema,
|
|
12
|
+
ReferencePropertySchema
|
|
13
|
+
} from './JsonSchema';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Removes properties from an object that are not defined in the JSON schema.
|
|
17
|
+
*
|
|
18
|
+
* **Intent:**
|
|
19
|
+
* This function ensures that objects conform to their schema definition by removing
|
|
20
|
+
* any properties that are not explicitly defined in the schema. It performs a deep
|
|
21
|
+
* cleanup, recursively processing nested objects, arrays, and schema references.
|
|
22
|
+
*
|
|
23
|
+
* **Use Cases:**
|
|
24
|
+
* - **Third-party API integrations**: When integrating with external services (e.g., Telegram)
|
|
25
|
+
* that may send additional fields you don't want to process, this function allows you
|
|
26
|
+
* to define a minimal schema and automatically strip unwanted properties.
|
|
27
|
+
* - **Data sanitization**: Clean up objects received from external sources or user input
|
|
28
|
+
* before validation or processing, ensuring only expected fields are present.
|
|
29
|
+
* - **Schema enforcement**: Enforce strict schema compliance by removing any properties
|
|
30
|
+
* that don't match the defined schema structure.
|
|
31
|
+
* - **Pre-validation cleanup**: Remove extraneous properties before schema validation to
|
|
32
|
+
* prevent validation errors from unexpected fields.
|
|
33
|
+
*
|
|
34
|
+
* **Behavior:**
|
|
35
|
+
* - Mutates the input object in-place (does not return a new object)
|
|
36
|
+
* - Recursively processes nested objects, arrays, and schema references ($ref)
|
|
37
|
+
* - Skips enum schemas (returns early without modification)
|
|
38
|
+
* - Only processes object values (skips null, undefined, and primitive values)
|
|
39
|
+
* - Handles array items by cleaning each object item according to the array's item schema
|
|
40
|
+
*
|
|
41
|
+
* @param object - The target object to clean up (mutated in-place)
|
|
42
|
+
* @param jsonSchema - The JSON schema defining allowed properties
|
|
43
|
+
* @param schemasMap - Optional map of schema IDs to schema definitions for resolving $ref references
|
|
44
|
+
*/
|
|
45
|
+
const cleanupAttributes = (
|
|
46
|
+
object: TargetObject,
|
|
47
|
+
jsonSchema: JsonSchema,
|
|
48
|
+
schemasMap: JsonSchemasMap = {}
|
|
49
|
+
) => {
|
|
50
|
+
const { enum: enumItems } = (jsonSchema as EnumSchema);
|
|
51
|
+
|
|
52
|
+
const isEnum = !!enumItems;
|
|
53
|
+
|
|
54
|
+
if (isEnum) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const objectSchema = (jsonSchema as ObjectSchema);
|
|
59
|
+
|
|
60
|
+
// Guard against malformed schemas without properties
|
|
61
|
+
if (!objectSchema.properties) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const fieldName in object) {
|
|
66
|
+
const property = objectSchema.properties[fieldName];
|
|
67
|
+
|
|
68
|
+
const isPropertyUndefined = isUndefined(property);
|
|
69
|
+
|
|
70
|
+
if (isPropertyUndefined) {
|
|
71
|
+
// NOTE: Delete object property if it's not defined in the object schema:
|
|
72
|
+
delete object[fieldName];
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { $ref: refSchemaId } = (property as ReferencePropertySchema);
|
|
77
|
+
|
|
78
|
+
const isReference = !isUndefined(refSchemaId);
|
|
79
|
+
|
|
80
|
+
if (isReference) {
|
|
81
|
+
const referenceSchema = got(schemasMap, refSchemaId, 'Schema "$PATH" not found');
|
|
82
|
+
const fieldValue = object[fieldName];
|
|
83
|
+
|
|
84
|
+
// Only recursively cleanup if the value is an object (not null, undefined, or primitive)
|
|
85
|
+
if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue)) {
|
|
86
|
+
cleanupAttributes(fieldValue as TargetObject, referenceSchema, schemasMap);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { type } = (property as ObjectPropertySchema | ArrayPropertySchema);
|
|
92
|
+
|
|
93
|
+
const isObject = type === 'object';
|
|
94
|
+
|
|
95
|
+
if (isObject) {
|
|
96
|
+
const { properties = {} } = (property as ObjectPropertySchema);
|
|
97
|
+
|
|
98
|
+
const fieldValue = object[fieldName];
|
|
99
|
+
|
|
100
|
+
const isObjectValue = fieldValue &&
|
|
101
|
+
typeof fieldValue === 'object' &&
|
|
102
|
+
!Array.isArray(fieldValue);
|
|
103
|
+
|
|
104
|
+
if (isObjectValue) {
|
|
105
|
+
const nestedJsonSchema = {
|
|
106
|
+
id: `${objectSchema.id}.${fieldName}.properties`,
|
|
107
|
+
properties
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
cleanupAttributes(fieldValue as TargetObject, nestedJsonSchema, schemasMap);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isArray = type === 'array';
|
|
116
|
+
|
|
117
|
+
if (isArray) {
|
|
118
|
+
const { items } = (property as ArrayPropertySchema);
|
|
119
|
+
|
|
120
|
+
const fieldValue = object[fieldName];
|
|
121
|
+
const isArrayValue = Array.isArray(fieldValue);
|
|
122
|
+
|
|
123
|
+
if (isArrayValue && items) {
|
|
124
|
+
const { $ref: itemRefSchemaId } = (items as ReferencePropertySchema);
|
|
125
|
+
|
|
126
|
+
const { properties: itemObjectProperties = {} } = (items as ObjectPropertySchema);
|
|
127
|
+
|
|
128
|
+
const isItemReference = !isUndefined(itemRefSchemaId);
|
|
129
|
+
|
|
130
|
+
const itemSchema = isItemReference
|
|
131
|
+
? got(schemasMap, itemRefSchemaId, 'Schema "$PATH" not found')
|
|
132
|
+
: {
|
|
133
|
+
id: `${objectSchema.id}.${fieldName}.items.properties`,
|
|
134
|
+
properties: itemObjectProperties
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
for (const item of fieldValue) {
|
|
138
|
+
const isObjectItem = item &&
|
|
139
|
+
typeof item === 'object' &&
|
|
140
|
+
!Array.isArray(item);
|
|
141
|
+
|
|
142
|
+
if (isObjectItem) {
|
|
143
|
+
cleanupAttributes(item as TargetObject, itemSchema, schemasMap);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default cleanupAttributes;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isObject, cloneDeep } from 'lodash';
|
|
2
|
+
|
|
3
|
+
import { type TargetObject } from './JsonSchema';
|
|
4
|
+
|
|
5
|
+
const { isArray } = Array;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Recursively removes null properties from an object.
|
|
9
|
+
*
|
|
10
|
+
* **Intent:**
|
|
11
|
+
* This function provides a deep cleanup of objects by removing all properties
|
|
12
|
+
* that have `null` values. It's designed to sanitize objects before validation,
|
|
13
|
+
* storage, or transmission by eliminating explicitly null fields.
|
|
14
|
+
*
|
|
15
|
+
* **Use Cases:**
|
|
16
|
+
* - **Pre-validation cleanup**: Remove null values before schema validation to
|
|
17
|
+
* prevent validation errors from optional fields that were explicitly set to null.
|
|
18
|
+
* This is particularly useful when `shouldCleanupNulls` is enabled in the Validator,
|
|
19
|
+
* allowing you to clean objects before `cleanupAttributes` removes undefined properties.
|
|
20
|
+
* - **Data sanitization**: Clean objects received from external sources (APIs, user input,
|
|
21
|
+
* databases) by removing null properties that may have been set during data transformation
|
|
22
|
+
* or migration processes.
|
|
23
|
+
* - **API response normalization**: Prepare objects for API responses by removing null fields,
|
|
24
|
+
* reducing payload size and ensuring consistent data structures across different endpoints.
|
|
25
|
+
* - **Database operations**: Clean objects before database storage or updates, removing
|
|
26
|
+
* null fields that might cause issues with database constraints or indexing.
|
|
27
|
+
* - **JSON serialization optimization**: Reduce JSON payload size by removing null properties
|
|
28
|
+
* before serialization, which is especially beneficial for large objects or high-frequency
|
|
29
|
+
* API calls.
|
|
30
|
+
* - **Optional field handling**: Remove explicitly null optional fields that weren't provided
|
|
31
|
+
* by the user, distinguishing between "field not provided" (undefined) and "field set to null".
|
|
32
|
+
*
|
|
33
|
+
* **Behavior:**
|
|
34
|
+
* - Returns a deep clone of the input object (does not mutate the original)
|
|
35
|
+
* - Recursively processes nested objects and arrays at all depth levels
|
|
36
|
+
* - Only removes properties with `null` values (preserves `undefined`, `0`, `false`, `''`, etc.)
|
|
37
|
+
* - Skips non-object values (returns early for primitives)
|
|
38
|
+
* - Handles arrays by recursively processing each item
|
|
39
|
+
* - Preserves object structure and non-null values exactly as they are
|
|
40
|
+
*
|
|
41
|
+
* @param target - The target object to clean (processed recursively, not mutated)
|
|
42
|
+
*/
|
|
43
|
+
const cleanupNulls = (target: TargetObject) => {
|
|
44
|
+
const shouldSkip = !isObject(target);
|
|
45
|
+
|
|
46
|
+
if (shouldSkip) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const key in target) {
|
|
51
|
+
const value = target[key];
|
|
52
|
+
|
|
53
|
+
if (isArray(value)) {
|
|
54
|
+
for (const item of value) {
|
|
55
|
+
cleanupNulls(item);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isObject(value)) {
|
|
62
|
+
cleanupNulls(value as Record<string, unknown>);
|
|
63
|
+
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isNull = value === null;
|
|
68
|
+
|
|
69
|
+
if (isNull) {
|
|
70
|
+
delete target[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns a deep copy of the object with all null properties removed.
|
|
77
|
+
*
|
|
78
|
+
* This is the main exported function that creates a clone of the input object
|
|
79
|
+
* and removes all null properties recursively before returning it.
|
|
80
|
+
*
|
|
81
|
+
* @param object - The object to clean (will be cloned, original is not modified)
|
|
82
|
+
* @returns A new object with all null properties removed recursively
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const dirty = {
|
|
87
|
+
* name: 'John',
|
|
88
|
+
* age: null,
|
|
89
|
+
* address: {
|
|
90
|
+
* street: 'Main St',
|
|
91
|
+
* zip: null
|
|
92
|
+
* }
|
|
93
|
+
* };
|
|
94
|
+
*
|
|
95
|
+
* const clean = cleanupNulls(dirty);
|
|
96
|
+
* // Result: { name: 'John', address: { street: 'Main St' } }
|
|
97
|
+
* // Original 'dirty' object is unchanged
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export default function (object: Record<string, unknown>) {
|
|
101
|
+
const clone = cloneDeep(object);
|
|
102
|
+
|
|
103
|
+
cleanupNulls(clone);
|
|
104
|
+
|
|
105
|
+
return clone;
|
|
106
|
+
};
|
|
@@ -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;
|