@kravc/schema 2.7.6 → 2.8.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +19 -14
  2. package/dist/CredentialFactory.d.ts +345 -0
  3. package/dist/CredentialFactory.d.ts.map +1 -0
  4. package/dist/CredentialFactory.js +381 -0
  5. package/dist/CredentialFactory.js.map +1 -0
  6. package/dist/Schema.d.ts +448 -0
  7. package/dist/Schema.d.ts.map +1 -0
  8. package/dist/Schema.js +506 -0
  9. package/dist/Schema.js.map +1 -0
  10. package/dist/ValidationError.d.ts +70 -0
  11. package/dist/ValidationError.d.ts.map +1 -0
  12. package/dist/ValidationError.js +78 -0
  13. package/dist/ValidationError.js.map +1 -0
  14. package/dist/Validator.d.ts +483 -0
  15. package/dist/Validator.d.ts.map +1 -0
  16. package/dist/Validator.js +570 -0
  17. package/dist/Validator.js.map +1 -0
  18. package/dist/helpers/JsonSchema.d.ts +99 -0
  19. package/dist/helpers/JsonSchema.d.ts.map +1 -0
  20. package/dist/helpers/JsonSchema.js +3 -0
  21. package/dist/helpers/JsonSchema.js.map +1 -0
  22. package/dist/helpers/cleanupAttributes.d.ts +34 -0
  23. package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
  24. package/dist/helpers/cleanupAttributes.js +113 -0
  25. package/dist/helpers/cleanupAttributes.js.map +1 -0
  26. package/dist/helpers/cleanupNulls.d.ts +27 -0
  27. package/dist/helpers/cleanupNulls.d.ts.map +1 -0
  28. package/dist/helpers/cleanupNulls.js +96 -0
  29. package/dist/helpers/cleanupNulls.js.map +1 -0
  30. package/dist/helpers/getReferenceIds.d.ts +169 -0
  31. package/dist/helpers/getReferenceIds.d.ts.map +1 -0
  32. package/dist/helpers/getReferenceIds.js +241 -0
  33. package/dist/helpers/getReferenceIds.js.map +1 -0
  34. package/dist/helpers/got.d.ts +60 -0
  35. package/dist/helpers/got.d.ts.map +1 -0
  36. package/dist/helpers/got.js +72 -0
  37. package/dist/helpers/got.js.map +1 -0
  38. package/dist/helpers/mapObjectProperties.d.ts +150 -0
  39. package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
  40. package/dist/helpers/mapObjectProperties.js +229 -0
  41. package/dist/helpers/mapObjectProperties.js.map +1 -0
  42. package/dist/helpers/normalizeAttributes.d.ts +213 -0
  43. package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
  44. package/dist/helpers/normalizeAttributes.js +243 -0
  45. package/dist/helpers/normalizeAttributes.js.map +1 -0
  46. package/dist/helpers/normalizeProperties.d.ts +168 -0
  47. package/dist/helpers/normalizeProperties.d.ts.map +1 -0
  48. package/dist/helpers/normalizeProperties.js +223 -0
  49. package/dist/helpers/normalizeProperties.js.map +1 -0
  50. package/dist/helpers/normalizeRequired.d.ts +159 -0
  51. package/dist/helpers/normalizeRequired.d.ts.map +1 -0
  52. package/dist/helpers/normalizeRequired.js +206 -0
  53. package/dist/helpers/normalizeRequired.js.map +1 -0
  54. package/dist/helpers/normalizeType.d.ts +81 -0
  55. package/dist/helpers/normalizeType.d.ts.map +1 -0
  56. package/dist/helpers/normalizeType.js +210 -0
  57. package/dist/helpers/normalizeType.js.map +1 -0
  58. package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
  59. package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
  60. package/dist/helpers/nullifyEmptyValues.js +191 -0
  61. package/dist/helpers/nullifyEmptyValues.js.map +1 -0
  62. package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
  63. package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
  64. package/dist/helpers/removeRequiredAndDefault.js +138 -0
  65. package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
  66. package/dist/helpers/validateId.d.ts +39 -0
  67. package/dist/helpers/validateId.d.ts.map +1 -0
  68. package/dist/helpers/validateId.js +51 -0
  69. package/dist/helpers/validateId.js.map +1 -0
  70. package/dist/index.d.ts +7 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +17 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/ld/documentLoader.d.ts +8 -0
  75. package/dist/ld/documentLoader.d.ts.map +1 -0
  76. package/dist/ld/documentLoader.js +24 -0
  77. package/dist/ld/documentLoader.js.map +1 -0
  78. package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
  79. package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
  80. package/dist/ld/getLinkedDataAttributeType.js +32 -0
  81. package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
  82. package/dist/ld/getLinkedDataContext.d.ts +19 -0
  83. package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
  84. package/dist/ld/getLinkedDataContext.js +50 -0
  85. package/dist/ld/getLinkedDataContext.js.map +1 -0
  86. package/eslint.config.mjs +32 -52
  87. package/examples/credentials/createAccountCredential.ts +27 -0
  88. package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
  89. package/examples/index.ts +7 -0
  90. package/examples/schemas/FavoriteItemSchema.ts +27 -0
  91. package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
  92. package/examples/schemas/PreferencesSchema.ts +29 -0
  93. package/examples/schemas/ProfileSchema.ts +91 -0
  94. package/examples/schemas/Status.yaml +3 -0
  95. package/examples/schemas/StatusSchema.ts +12 -0
  96. package/jest.config.mjs +5 -0
  97. package/package.json +27 -20
  98. package/src/CredentialFactory.ts +392 -0
  99. package/src/Schema.ts +583 -0
  100. package/src/ValidationError.ts +90 -0
  101. package/src/Validator.ts +603 -0
  102. package/src/__tests__/CredentialFactory.test.ts +588 -0
  103. package/src/__tests__/Schema.test.ts +371 -0
  104. package/src/__tests__/ValidationError.test.ts +235 -0
  105. package/src/__tests__/Validator.test.ts +787 -0
  106. package/src/helpers/JsonSchema.ts +119 -0
  107. package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
  108. package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
  109. package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
  110. package/src/helpers/__tests__/got.test.ts +193 -0
  111. package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
  112. package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
  113. package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
  114. package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
  115. package/src/helpers/__tests__/normalizeType.test.ts +772 -0
  116. package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
  117. package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
  118. package/src/helpers/__tests__/validateId.test.ts +118 -0
  119. package/src/helpers/cleanupAttributes.ts +151 -0
  120. package/src/helpers/cleanupNulls.ts +106 -0
  121. package/src/helpers/getReferenceIds.ts +273 -0
  122. package/src/helpers/got.ts +73 -0
  123. package/src/helpers/mapObjectProperties.ts +272 -0
  124. package/src/helpers/normalizeAttributes.ts +247 -0
  125. package/src/helpers/normalizeProperties.ts +249 -0
  126. package/src/helpers/normalizeRequired.ts +233 -0
  127. package/src/helpers/normalizeType.ts +235 -0
  128. package/src/helpers/nullifyEmptyValues.ts +207 -0
  129. package/src/helpers/removeRequiredAndDefault.ts +151 -0
  130. package/src/helpers/validateId.ts +53 -0
  131. package/src/index.ts +13 -0
  132. package/src/ld/__tests__/documentLoader.test.ts +57 -0
  133. package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
  134. package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
  135. package/src/ld/documentLoader.ts +28 -0
  136. package/src/ld/getLinkedDataAttributeType.ts +46 -0
  137. package/src/ld/getLinkedDataContext.ts +80 -0
  138. package/tsconfig.json +27 -0
  139. package/types/credentials-context.d.ts +14 -0
  140. package/types/security-context.d.ts +6 -0
  141. package/examples/Status.yaml +0 -3
  142. package/examples/createAccountCredential.js +0 -27
  143. package/examples/createMineSweeperScoreCredential.js +0 -63
  144. package/examples/index.js +0 -9
  145. package/src/CredentialFactory.js +0 -67
  146. package/src/CredentialFactory.spec.js +0 -131
  147. package/src/Schema.js +0 -104
  148. package/src/Schema.spec.js +0 -172
  149. package/src/ValidationError.js +0 -31
  150. package/src/Validator.js +0 -128
  151. package/src/Validator.spec.js +0 -355
  152. package/src/helpers/cleanupAttributes.js +0 -71
  153. package/src/helpers/cleanupNulls.js +0 -42
  154. package/src/helpers/getReferenceIds.js +0 -71
  155. package/src/helpers/mapObject.js +0 -65
  156. package/src/helpers/normalizeAttributes.js +0 -28
  157. package/src/helpers/normalizeProperties.js +0 -61
  158. package/src/helpers/normalizeRequired.js +0 -37
  159. package/src/helpers/normalizeType.js +0 -41
  160. package/src/helpers/nullifyEmptyValues.js +0 -57
  161. package/src/helpers/removeRequiredAndDefault.js +0 -30
  162. package/src/helpers/validateId.js +0 -19
  163. package/src/index.d.ts +0 -25
  164. package/src/index.js +0 -8
  165. package/src/ld/documentLoader.js +0 -25
  166. package/src/ld/documentLoader.spec.js +0 -12
  167. package/src/ld/getLinkedDataContext.js +0 -63
  168. package/src/ld/getLinkedDataType.js +0 -38
  169. /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
  170. /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
@@ -0,0 +1,57 @@
1
+ import documentLoader from '../documentLoader';
2
+ import { constants } from 'credentials-context';
3
+
4
+ describe('documentLoader(documentUrl)', () => {
5
+ describe('successful loading', () => {
6
+ it('should return document for valid credentials context URL', () => {
7
+ const result = documentLoader(constants.CREDENTIALS_CONTEXT_V1_URL);
8
+
9
+ expect(result).toHaveProperty('document');
10
+ expect(result).toHaveProperty('contextUrl', null);
11
+ expect(result).toHaveProperty('documentUrl', constants.CREDENTIALS_CONTEXT_V1_URL);
12
+ expect(result.document).toBeDefined();
13
+ expect(typeof result.document).toBe('object');
14
+ });
15
+
16
+ it('should handle URL with fragment', () => {
17
+ const urlWithFragment = `${constants.CREDENTIALS_CONTEXT_V1_URL}#VerifiableCredential`;
18
+ const result = documentLoader(urlWithFragment);
19
+
20
+ expect(result).toHaveProperty('document');
21
+ expect(result).toHaveProperty('documentUrl', urlWithFragment);
22
+ expect(result.document).toBeDefined();
23
+ });
24
+
25
+ it('should return correct structure', () => {
26
+ const result = documentLoader(constants.CREDENTIALS_CONTEXT_V1_URL);
27
+
28
+ expect(result).toEqual({
29
+ document: expect.any(Object),
30
+ contextUrl: null,
31
+ documentUrl: constants.CREDENTIALS_CONTEXT_V1_URL
32
+ });
33
+ });
34
+ });
35
+
36
+ describe('error handling', () => {
37
+ it('should throw error if context not found', () => {
38
+ expect(() => {
39
+ documentLoader('https://example.com/unknown-context');
40
+ }).toThrow('Custom context "https://example.com/unknown-context" is not supported');
41
+ });
42
+
43
+ it('should throw error with full URL including fragment when context not found', () => {
44
+ const urlWithFragment = 'https://example.com/unknown-context#fragment';
45
+
46
+ expect(() => {
47
+ documentLoader(urlWithFragment);
48
+ }).toThrow('Custom context "https://example.com/unknown-context#fragment" is not supported');
49
+ });
50
+
51
+ it('should throw error for empty string', () => {
52
+ expect(() => {
53
+ documentLoader('');
54
+ }).toThrow('Custom context "" is not supported');
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,212 @@
1
+ import getLinkedDataAttributeType, { type PropertySchema } from '../getLinkedDataAttributeType';
2
+
3
+ describe('getLinkedDataAttributeType(propertySchema)', () => {
4
+ describe('when @type is defined', () => {
5
+ it('should return the @type value when it is a string', () => {
6
+ const propertySchema: PropertySchema = {
7
+ '@type': 'schema:Text',
8
+ type: 'string',
9
+ };
10
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Text');
11
+ });
12
+
13
+ it('should return the @type value even when type and format are present', () => {
14
+ const propertySchema: PropertySchema = {
15
+ '@type': 'schema:CustomType',
16
+ type: 'number',
17
+ format: 'date',
18
+ };
19
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:CustomType');
20
+ });
21
+
22
+ it('should return the @type value for integer type', () => {
23
+ const propertySchema: PropertySchema = {
24
+ '@type': 'schema:CustomInteger',
25
+ type: 'integer',
26
+ };
27
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:CustomInteger');
28
+ });
29
+ });
30
+
31
+ describe('when type is integer', () => {
32
+ it('should return schema:Integer for integer type', () => {
33
+ const propertySchema: PropertySchema = {
34
+ type: 'integer',
35
+ };
36
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Integer');
37
+ });
38
+
39
+ it('should return schema:Integer even when format is present', () => {
40
+ const propertySchema: PropertySchema = {
41
+ type: 'integer',
42
+ format: 'int32',
43
+ };
44
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Integer');
45
+ });
46
+ });
47
+
48
+ describe('when type is number', () => {
49
+ it('should return schema:Number for number type', () => {
50
+ const propertySchema: PropertySchema = {
51
+ type: 'number',
52
+ };
53
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Number');
54
+ });
55
+
56
+ it('should return schema:Number even when format is present', () => {
57
+ const propertySchema: PropertySchema = {
58
+ type: 'number',
59
+ format: 'float',
60
+ };
61
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Number');
62
+ });
63
+ });
64
+
65
+ describe('when format is date', () => {
66
+ it('should return schema:Date for date format', () => {
67
+ const propertySchema: PropertySchema = {
68
+ type: 'string',
69
+ format: 'date',
70
+ };
71
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Date');
72
+ });
73
+
74
+ it('should return schema:Date regardless of type when format is date', () => {
75
+ const propertySchema: PropertySchema = {
76
+ type: 'object',
77
+ format: 'date',
78
+ };
79
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Date');
80
+ });
81
+ });
82
+
83
+ describe('when format is date-time', () => {
84
+ it('should return schema:DateTime for date-time format', () => {
85
+ const propertySchema: PropertySchema = {
86
+ type: 'string',
87
+ format: 'date-time',
88
+ };
89
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:DateTime');
90
+ });
91
+
92
+ it('should return schema:DateTime when format is date-time and type is string', () => {
93
+ const propertySchema: PropertySchema = {
94
+ type: 'string',
95
+ format: 'date-time',
96
+ };
97
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:DateTime');
98
+ });
99
+
100
+ it('should prioritize type over format (number type wins over date-time format)', () => {
101
+ const propertySchema: PropertySchema = {
102
+ type: 'number',
103
+ format: 'date-time',
104
+ };
105
+ // The function checks type before format, so number type takes priority
106
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Number');
107
+ });
108
+ });
109
+
110
+ describe('when no match is found', () => {
111
+ it('should return undefined for string type without format', () => {
112
+ const propertySchema: PropertySchema = {
113
+ type: 'string',
114
+ };
115
+ expect(getLinkedDataAttributeType(propertySchema)).toBeUndefined();
116
+ });
117
+
118
+ it('should return undefined for boolean type', () => {
119
+ const propertySchema: PropertySchema = {
120
+ type: 'boolean',
121
+ };
122
+ expect(getLinkedDataAttributeType(propertySchema)).toBeUndefined();
123
+ });
124
+
125
+ it('should return undefined for array type', () => {
126
+ const propertySchema: PropertySchema = {
127
+ type: 'array',
128
+ };
129
+ expect(getLinkedDataAttributeType(propertySchema)).toBeUndefined();
130
+ });
131
+
132
+ it('should return undefined for object type', () => {
133
+ const propertySchema: PropertySchema = {
134
+ type: 'object',
135
+ };
136
+ expect(getLinkedDataAttributeType(propertySchema)).toBeUndefined();
137
+ });
138
+
139
+ it('should return undefined for other formats', () => {
140
+ const propertySchema: PropertySchema = {
141
+ type: 'string',
142
+ format: 'email',
143
+ };
144
+ expect(getLinkedDataAttributeType(propertySchema)).toBeUndefined();
145
+ });
146
+
147
+ it('should return undefined for empty object', () => {
148
+ const propertySchema: PropertySchema = {} as PropertySchema;
149
+ expect(getLinkedDataAttributeType(propertySchema)).toBeUndefined();
150
+ });
151
+
152
+ it('should return undefined when only $ref is present', () => {
153
+ const propertySchema: PropertySchema = {
154
+ $ref: '#/definitions/SomeType',
155
+ };
156
+ expect(getLinkedDataAttributeType(propertySchema)).toBeUndefined();
157
+ });
158
+ });
159
+
160
+ describe('priority order', () => {
161
+ it('should prioritize @type over type integer', () => {
162
+ const propertySchema: PropertySchema = {
163
+ '@type': 'schema:Custom',
164
+ type: 'integer',
165
+ };
166
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Custom');
167
+ });
168
+
169
+ it('should prioritize @type over format date', () => {
170
+ const propertySchema: PropertySchema = {
171
+ '@type': 'schema:Custom',
172
+ format: 'date',
173
+ };
174
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Custom');
175
+ });
176
+
177
+ it('should prioritize integer type over number type', () => {
178
+ const propertySchema: PropertySchema = {
179
+ type: 'integer',
180
+ format: 'number',
181
+ };
182
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Integer');
183
+ });
184
+
185
+ it('should prioritize integer type over date format', () => {
186
+ const propertySchema: PropertySchema = {
187
+ type: 'integer',
188
+ format: 'date',
189
+ };
190
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Integer');
191
+ });
192
+
193
+ it('should prioritize number type over date format', () => {
194
+ const propertySchema: PropertySchema = {
195
+ type: 'number',
196
+ format: 'date',
197
+ };
198
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Number');
199
+ });
200
+
201
+ it('should prioritize date format over date-time format when both are present (date checked first)', () => {
202
+ // NOTE: This tests the actual implementation order.
203
+ // The function checks date before date-time, so date would win if both were somehow set.
204
+ // But in practice, format can only be one value, so this is more of an edge case test.
205
+ const propertySchema: PropertySchema = {
206
+ type: 'string',
207
+ format: 'date',
208
+ };
209
+ expect(getLinkedDataAttributeType(propertySchema)).toBe('schema:Date');
210
+ });
211
+ });
212
+ });
@@ -0,0 +1,378 @@
1
+ import getLinkedDataContext, { type PropertySchema } from '../getLinkedDataContext';
2
+
3
+ describe('getLinkedDataContext(properties, vocabUri)', () => {
4
+ describe('basic functionality', () => {
5
+ it('should create context with @vocab, @version, and @protected', () => {
6
+ const properties: Record<string, PropertySchema> = {};
7
+ const vocabUri = 'https://example.com/vocab';
8
+
9
+ const result = getLinkedDataContext(properties, vocabUri);
10
+
11
+ expect(result).toHaveProperty('@vocab', 'https://example.com/vocab#');
12
+ expect(result).toHaveProperty('@version', 1.1);
13
+ expect(result).toHaveProperty('@protected', true);
14
+ });
15
+
16
+ it('should include properties in context with @id', () => {
17
+ const properties: Record<string, PropertySchema> = {
18
+ name: { type: 'string' },
19
+ active: { type: 'boolean' },
20
+ };
21
+ const vocabUri = 'https://example.com/vocab';
22
+
23
+ const result = getLinkedDataContext(properties, vocabUri);
24
+
25
+ expect(result.name).toEqual({ '@id': 'name' });
26
+ expect(result.active).toEqual({ '@id': 'active' });
27
+ });
28
+ });
29
+
30
+ describe('protected properties', () => {
31
+ it('should skip id property', () => {
32
+ const properties: Record<string, PropertySchema> = {
33
+ id: { type: 'string' },
34
+ name: { type: 'string' },
35
+ };
36
+ const vocabUri = 'https://example.com/vocab';
37
+
38
+ const result = getLinkedDataContext(properties, vocabUri);
39
+
40
+ expect(result).not.toHaveProperty('id');
41
+ expect(result.name).toEqual({ '@id': 'name' });
42
+ });
43
+
44
+ it('should skip type property', () => {
45
+ const properties: Record<string, PropertySchema> = {
46
+ type: { type: 'string' },
47
+ name: { type: 'string' },
48
+ };
49
+ const vocabUri = 'https://example.com/vocab';
50
+
51
+ const result = getLinkedDataContext(properties, vocabUri);
52
+
53
+ expect(result).not.toHaveProperty('type');
54
+ expect(result.name).toEqual({ '@id': 'name' });
55
+ });
56
+
57
+ it('should skip schema property', () => {
58
+ const properties: Record<string, PropertySchema> = {
59
+ schema: { type: 'string' },
60
+ name: { type: 'string' },
61
+ };
62
+ // Use schema.org vocab so the header doesn't add a 'schema' property
63
+ const vocabUri = 'https://schema.org/';
64
+
65
+ const result = getLinkedDataContext(properties, vocabUri);
66
+
67
+ // The input 'schema' property should be skipped (it's protected)
68
+ expect(result).not.toHaveProperty('schema');
69
+ expect(result.name).toEqual({ '@id': 'name' });
70
+ });
71
+
72
+ it('should skip properties starting with @', () => {
73
+ const properties: Record<string, PropertySchema> = {
74
+ '@context': { type: 'string' },
75
+ '@type': { type: 'string' },
76
+ '@id': { type: 'string' },
77
+ name: { type: 'string' },
78
+ };
79
+ const vocabUri = 'https://example.com/vocab';
80
+
81
+ const result = getLinkedDataContext(properties, vocabUri);
82
+
83
+ expect(result).not.toHaveProperty('@context');
84
+ expect(result).not.toHaveProperty('@type');
85
+ expect(result).not.toHaveProperty('@id');
86
+ expect(result.name).toEqual({ '@id': 'name' });
87
+ });
88
+
89
+ it('should skip all protected properties together', () => {
90
+ const properties: Record<string, PropertySchema> = {
91
+ id: { type: 'string' },
92
+ type: { type: 'string' },
93
+ schema: { type: 'string' },
94
+ '@custom': { type: 'string' },
95
+ name: { type: 'string' },
96
+ };
97
+ // Use schema.org vocab so the header doesn't add a 'schema' property
98
+ const vocabUri = 'https://schema.org/';
99
+
100
+ const result = getLinkedDataContext(properties, vocabUri);
101
+
102
+ // All protected properties from input should be skipped
103
+ expect(result).not.toHaveProperty('id');
104
+ expect(result).not.toHaveProperty('type');
105
+ expect(result).not.toHaveProperty('schema');
106
+ expect(result).not.toHaveProperty('@custom');
107
+ expect(result.name).toEqual({ '@id': 'name' });
108
+ });
109
+ });
110
+
111
+ describe('properties with $ref', () => {
112
+ it('should include property with $ref but without @type', () => {
113
+ const properties: Record<string, PropertySchema> = {
114
+ reference: { $ref: '#/definitions/SomeType' },
115
+ };
116
+ const vocabUri = 'https://example.com/vocab';
117
+
118
+ const result = getLinkedDataContext(properties, vocabUri);
119
+
120
+ expect(result.reference).toEqual({ '@id': 'reference' });
121
+ expect(result.reference).not.toHaveProperty('@type');
122
+ });
123
+
124
+ it('should not add @type even if property has type and format', () => {
125
+ const properties: Record<string, PropertySchema> = {
126
+ reference: {
127
+ $ref: '#/definitions/SomeType',
128
+ type: 'integer',
129
+ format: 'int32',
130
+ },
131
+ };
132
+ const vocabUri = 'https://example.com/vocab';
133
+
134
+ const result = getLinkedDataContext(properties, vocabUri);
135
+
136
+ expect(result.reference).toEqual({ '@id': 'reference' });
137
+ expect(result.reference).not.toHaveProperty('@type');
138
+ });
139
+ });
140
+
141
+ describe('properties with linked data types', () => {
142
+ it('should add @type for integer type', () => {
143
+ const properties: Record<string, PropertySchema> = {
144
+ age: { type: 'integer' },
145
+ };
146
+ const vocabUri = 'https://example.com/vocab';
147
+
148
+ const result = getLinkedDataContext(properties, vocabUri);
149
+
150
+ expect(result.age).toEqual({
151
+ '@id': 'age',
152
+ '@type': 'schema:Integer',
153
+ });
154
+ });
155
+
156
+ it('should add @type for number type', () => {
157
+ const properties: Record<string, PropertySchema> = {
158
+ price: { type: 'number' },
159
+ };
160
+ const vocabUri = 'https://example.com/vocab';
161
+
162
+ const result = getLinkedDataContext(properties, vocabUri);
163
+
164
+ expect(result.price).toEqual({
165
+ '@id': 'price',
166
+ '@type': 'schema:Number',
167
+ });
168
+ });
169
+
170
+ it('should add @type for date format', () => {
171
+ const properties: Record<string, PropertySchema> = {
172
+ birthDate: { type: 'string', format: 'date' },
173
+ };
174
+ const vocabUri = 'https://example.com/vocab';
175
+
176
+ const result = getLinkedDataContext(properties, vocabUri);
177
+
178
+ expect(result.birthDate).toEqual({
179
+ '@id': 'birthDate',
180
+ '@type': 'schema:Date',
181
+ });
182
+ });
183
+
184
+ it('should add @type for date-time format', () => {
185
+ const properties: Record<string, PropertySchema> = {
186
+ createdAt: { type: 'string', format: 'date-time' },
187
+ };
188
+ const vocabUri = 'https://example.com/vocab';
189
+
190
+ const result = getLinkedDataContext(properties, vocabUri);
191
+
192
+ expect(result.createdAt).toEqual({
193
+ '@id': 'createdAt',
194
+ '@type': 'schema:DateTime',
195
+ });
196
+ });
197
+
198
+ it('should add @type when @type is explicitly defined', () => {
199
+ const properties: Record<string, PropertySchema> = {
200
+ custom: { '@type': 'schema:CustomType', type: 'string' },
201
+ };
202
+ const vocabUri = 'https://example.com/vocab';
203
+
204
+ const result = getLinkedDataContext(properties, vocabUri);
205
+
206
+ expect(result.custom).toEqual({
207
+ '@id': 'custom',
208
+ '@type': 'schema:CustomType',
209
+ });
210
+ });
211
+
212
+ it('should not add @type when getLinkedDataType returns undefined', () => {
213
+ const properties: Record<string, PropertySchema> = {
214
+ name: { type: 'string' },
215
+ active: { type: 'boolean' },
216
+ };
217
+ const vocabUri = 'https://example.com/vocab';
218
+
219
+ const result = getLinkedDataContext(properties, vocabUri);
220
+
221
+ expect(result.name).toEqual({ '@id': 'name' });
222
+ expect(result.name).not.toHaveProperty('@type');
223
+ expect(result.active).toEqual({ '@id': 'active' });
224
+ expect(result.active).not.toHaveProperty('@type');
225
+ });
226
+ });
227
+
228
+ describe('vocab URI handling', () => {
229
+ it('should append # to vocab URI that does not end with / or #', () => {
230
+ const properties: Record<string, PropertySchema> = {};
231
+ const vocabUri = 'https://example.com/vocab';
232
+
233
+ const result = getLinkedDataContext(properties, vocabUri);
234
+
235
+ expect(result['@vocab']).toBe('https://example.com/vocab#');
236
+ });
237
+
238
+ it('should not append # to vocab URI ending with /', () => {
239
+ const properties: Record<string, PropertySchema> = {};
240
+ const vocabUri = 'https://example.com/vocab/';
241
+
242
+ const result = getLinkedDataContext(properties, vocabUri);
243
+
244
+ expect(result['@vocab']).toBe('https://example.com/vocab/');
245
+ });
246
+
247
+ it('should not append # to vocab URI ending with #', () => {
248
+ const properties: Record<string, PropertySchema> = {};
249
+ const vocabUri = 'https://example.com/vocab#';
250
+
251
+ const result = getLinkedDataContext(properties, vocabUri);
252
+
253
+ expect(result['@vocab']).toBe('https://example.com/vocab#');
254
+ });
255
+ });
256
+
257
+ describe('schema.org domain handling', () => {
258
+ it('should not add schema property when vocab is schema.org', () => {
259
+ const properties: Record<string, PropertySchema> = {};
260
+ const vocabUri = 'https://schema.org/';
261
+
262
+ const result = getLinkedDataContext(properties, vocabUri);
263
+
264
+ expect(result).not.toHaveProperty('schema');
265
+ expect(result['@vocab']).toBe('https://schema.org/');
266
+ });
267
+
268
+ it('should add schema property when vocab is schema.org with # (exact match required)', () => {
269
+ const properties: Record<string, PropertySchema> = {};
270
+ const vocabUri = 'https://schema.org/#';
271
+
272
+ const result = getLinkedDataContext(properties, vocabUri);
273
+
274
+ // Note: The implementation checks if vocab === 'https://schema.org/' exactly,
275
+ // so 'https://schema.org/#' will not match and schema will be added
276
+ // This tests the actual behavior of the implementation
277
+ expect(result).toHaveProperty('schema', 'https://schema.org/');
278
+ expect(result['@vocab']).toBe('https://schema.org/#');
279
+ });
280
+
281
+ it('should add schema property when vocab is not schema.org', () => {
282
+ const properties: Record<string, PropertySchema> = {};
283
+ const vocabUri = 'https://example.com/vocab';
284
+
285
+ const result = getLinkedDataContext(properties, vocabUri);
286
+
287
+ expect(result).toHaveProperty('schema', 'https://schema.org/');
288
+ });
289
+
290
+ it('should add schema property for custom vocab URI', () => {
291
+ const properties: Record<string, PropertySchema> = {};
292
+ const vocabUri = 'https://custom-domain.com/ontology';
293
+
294
+ const result = getLinkedDataContext(properties, vocabUri);
295
+
296
+ expect(result).toHaveProperty('schema', 'https://schema.org/');
297
+ });
298
+ });
299
+
300
+ describe('mixed scenarios', () => {
301
+ it('should handle complex properties object with various types', () => {
302
+ const properties: Record<string, PropertySchema> = {
303
+ id: { type: 'string' }, // protected
304
+ type: { type: 'string' }, // protected
305
+ name: { type: 'string' }, // no @type
306
+ age: { type: 'integer' }, // schema:Integer
307
+ price: { type: 'number' }, // schema:Number
308
+ birthDate: { type: 'string', format: 'date' }, // schema:Date
309
+ createdAt: { type: 'string', format: 'date-time' }, // schema:DateTime
310
+ reference: { $ref: '#/definitions/Person' }, // $ref, no @type
311
+ custom: { '@type': 'schema:CustomType' }, // explicit @type
312
+ active: { type: 'boolean' }, // no @type
313
+ '@context': { type: 'string' }, // protected
314
+ };
315
+ const vocabUri = 'https://example.com/vocab';
316
+
317
+ const result = getLinkedDataContext(properties, vocabUri);
318
+
319
+ // Check header
320
+ expect(result['@vocab']).toBe('https://example.com/vocab#');
321
+ expect(result['@version']).toBe(1.1);
322
+ expect(result['@protected']).toBe(true);
323
+ expect(result.schema).toBe('https://schema.org/');
324
+
325
+ // Check protected properties are skipped
326
+ expect(result).not.toHaveProperty('id');
327
+ expect(result).not.toHaveProperty('type');
328
+ expect(result).not.toHaveProperty('@context');
329
+
330
+ // Check properties without @type
331
+ expect(result.name).toEqual({ '@id': 'name' });
332
+ expect(result.active).toEqual({ '@id': 'active' });
333
+
334
+ // Check properties with @type
335
+ expect(result.age).toEqual({ '@id': 'age', '@type': 'schema:Integer' });
336
+ expect(result.price).toEqual({ '@id': 'price', '@type': 'schema:Number' });
337
+ expect(result.birthDate).toEqual({ '@id': 'birthDate', '@type': 'schema:Date' });
338
+ expect(result.createdAt).toEqual({ '@id': 'createdAt', '@type': 'schema:DateTime' });
339
+ expect(result.custom).toEqual({ '@id': 'custom', '@type': 'schema:CustomType' });
340
+
341
+ // Check property with $ref
342
+ expect(result.reference).toEqual({ '@id': 'reference' });
343
+ });
344
+
345
+ it('should handle empty properties object', () => {
346
+ const properties: Record<string, PropertySchema> = {};
347
+ const vocabUri = 'https://example.com/vocab';
348
+
349
+ const result = getLinkedDataContext(properties, vocabUri);
350
+
351
+ expect(result).toEqual({
352
+ '@vocab': 'https://example.com/vocab#',
353
+ '@version': 1.1,
354
+ '@protected': true,
355
+ schema: 'https://schema.org/',
356
+ });
357
+ });
358
+
359
+ it('should handle properties object with only protected properties', () => {
360
+ const properties: Record<string, PropertySchema> = {
361
+ id: { type: 'string' },
362
+ type: { type: 'string' },
363
+ schema: { type: 'string' },
364
+ '@context': { type: 'string' },
365
+ };
366
+ const vocabUri = 'https://example.com/vocab';
367
+
368
+ const result = getLinkedDataContext(properties, vocabUri);
369
+
370
+ expect(result).toEqual({
371
+ '@vocab': 'https://example.com/vocab#',
372
+ '@version': 1.1,
373
+ '@protected': true,
374
+ schema: 'https://schema.org/',
375
+ });
376
+ });
377
+ });
378
+ });
@@ -0,0 +1,28 @@
1
+ import { contexts as securityContextsMap } from 'security-context';
2
+ import { contexts as credentialsContextsMap } from 'credentials-context';
3
+
4
+ const CONTEXTS = new Map([
5
+ ...securityContextsMap,
6
+ ...credentialsContextsMap
7
+ ]);
8
+
9
+ /** Returns document from the context via url */
10
+ const documentLoader = (documentUrl: string) => {
11
+ const contextUrl = null;
12
+
13
+ const [ url ] = documentUrl.split('#');
14
+
15
+ const document = CONTEXTS.get(url);
16
+
17
+ if (!document) {
18
+ throw new Error(`Custom context "${documentUrl}" is not supported`);
19
+ }
20
+
21
+ return {
22
+ document,
23
+ contextUrl,
24
+ documentUrl
25
+ };
26
+ };
27
+
28
+ export default documentLoader;