@kravc/schema 2.7.6 → 2.8.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -14
- package/dist/CredentialFactory.d.ts +345 -0
- package/dist/CredentialFactory.d.ts.map +1 -0
- package/dist/CredentialFactory.js +381 -0
- package/dist/CredentialFactory.js.map +1 -0
- package/dist/Schema.d.ts +448 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +506 -0
- package/dist/Schema.js.map +1 -0
- package/dist/ValidationError.d.ts +70 -0
- package/dist/ValidationError.d.ts.map +1 -0
- package/dist/ValidationError.js +78 -0
- package/dist/ValidationError.js.map +1 -0
- package/dist/Validator.d.ts +483 -0
- package/dist/Validator.d.ts.map +1 -0
- package/dist/Validator.js +570 -0
- package/dist/Validator.js.map +1 -0
- package/dist/helpers/JsonSchema.d.ts +99 -0
- package/dist/helpers/JsonSchema.d.ts.map +1 -0
- package/dist/helpers/JsonSchema.js +3 -0
- package/dist/helpers/JsonSchema.js.map +1 -0
- package/dist/helpers/cleanupAttributes.d.ts +34 -0
- package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
- package/dist/helpers/cleanupAttributes.js +113 -0
- package/dist/helpers/cleanupAttributes.js.map +1 -0
- package/dist/helpers/cleanupNulls.d.ts +27 -0
- package/dist/helpers/cleanupNulls.d.ts.map +1 -0
- package/dist/helpers/cleanupNulls.js +96 -0
- package/dist/helpers/cleanupNulls.js.map +1 -0
- package/dist/helpers/createSchemasMap.d.ts +67 -0
- package/dist/helpers/createSchemasMap.d.ts.map +1 -0
- package/dist/helpers/createSchemasMap.js +200 -0
- package/dist/helpers/createSchemasMap.js.map +1 -0
- package/dist/helpers/getReferenceIds.d.ts +169 -0
- package/dist/helpers/getReferenceIds.d.ts.map +1 -0
- package/dist/helpers/getReferenceIds.js +241 -0
- package/dist/helpers/getReferenceIds.js.map +1 -0
- package/dist/helpers/got.d.ts +60 -0
- package/dist/helpers/got.d.ts.map +1 -0
- package/dist/helpers/got.js +72 -0
- package/dist/helpers/got.js.map +1 -0
- package/dist/helpers/mapObjectProperties.d.ts +150 -0
- package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
- package/dist/helpers/mapObjectProperties.js +229 -0
- package/dist/helpers/mapObjectProperties.js.map +1 -0
- package/dist/helpers/normalizeAttributes.d.ts +213 -0
- package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
- package/dist/helpers/normalizeAttributes.js +243 -0
- package/dist/helpers/normalizeAttributes.js.map +1 -0
- package/dist/helpers/normalizeProperties.d.ts +168 -0
- package/dist/helpers/normalizeProperties.d.ts.map +1 -0
- package/dist/helpers/normalizeProperties.js +223 -0
- package/dist/helpers/normalizeProperties.js.map +1 -0
- package/dist/helpers/normalizeRequired.d.ts +159 -0
- package/dist/helpers/normalizeRequired.d.ts.map +1 -0
- package/dist/helpers/normalizeRequired.js +206 -0
- package/dist/helpers/normalizeRequired.js.map +1 -0
- package/dist/helpers/normalizeType.d.ts +81 -0
- package/dist/helpers/normalizeType.d.ts.map +1 -0
- package/dist/helpers/normalizeType.js +210 -0
- package/dist/helpers/normalizeType.js.map +1 -0
- package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
- package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
- package/dist/helpers/nullifyEmptyValues.js +191 -0
- package/dist/helpers/nullifyEmptyValues.js.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.js +138 -0
- package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
- package/dist/helpers/validateId.d.ts +39 -0
- package/dist/helpers/validateId.d.ts.map +1 -0
- package/dist/helpers/validateId.js +51 -0
- package/dist/helpers/validateId.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/ld/documentLoader.d.ts +8 -0
- package/dist/ld/documentLoader.d.ts.map +1 -0
- package/dist/ld/documentLoader.js +24 -0
- package/dist/ld/documentLoader.js.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.js +32 -0
- package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
- package/dist/ld/getLinkedDataContext.d.ts +19 -0
- package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
- package/dist/ld/getLinkedDataContext.js +50 -0
- package/dist/ld/getLinkedDataContext.js.map +1 -0
- package/eslint.config.mjs +32 -52
- package/examples/credentials/createAccountCredential.ts +27 -0
- package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
- package/examples/index.ts +7 -0
- package/examples/schemas/FavoriteItemSchema.ts +27 -0
- package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
- package/examples/schemas/PreferencesSchema.ts +29 -0
- package/examples/schemas/ProfileSchema.ts +91 -0
- package/examples/schemas/Status.yaml +3 -0
- package/examples/schemas/StatusSchema.ts +12 -0
- package/jest.config.mjs +5 -0
- package/package.json +27 -20
- package/src/CredentialFactory.ts +392 -0
- package/src/Schema.ts +583 -0
- package/src/ValidationError.ts +90 -0
- package/src/Validator.ts +603 -0
- package/src/__tests__/CredentialFactory.test.ts +588 -0
- package/src/__tests__/Schema.test.ts +371 -0
- package/src/__tests__/ValidationError.test.ts +235 -0
- package/src/__tests__/Validator.test.ts +787 -0
- package/src/helpers/JsonSchema.ts +119 -0
- package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
- package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
- package/src/helpers/__tests__/createSchemasMap.test.ts +238 -0
- package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
- package/src/helpers/__tests__/got.test.ts +193 -0
- package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
- package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
- package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
- package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
- package/src/helpers/__tests__/normalizeType.test.ts +772 -0
- package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
- package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
- package/src/helpers/__tests__/validateId.test.ts +118 -0
- package/src/helpers/cleanupAttributes.ts +151 -0
- package/src/helpers/cleanupNulls.ts +106 -0
- package/src/helpers/createSchemasMap.ts +212 -0
- package/src/helpers/getReferenceIds.ts +273 -0
- package/src/helpers/got.ts +73 -0
- package/src/helpers/mapObjectProperties.ts +272 -0
- package/src/helpers/normalizeAttributes.ts +247 -0
- package/src/helpers/normalizeProperties.ts +249 -0
- package/src/helpers/normalizeRequired.ts +233 -0
- package/src/helpers/normalizeType.ts +235 -0
- package/src/helpers/nullifyEmptyValues.ts +207 -0
- package/src/helpers/removeRequiredAndDefault.ts +151 -0
- package/src/helpers/validateId.ts +53 -0
- package/src/index.ts +17 -0
- package/src/ld/__tests__/documentLoader.test.ts +57 -0
- package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
- package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
- package/src/ld/documentLoader.ts +28 -0
- package/src/ld/getLinkedDataAttributeType.ts +46 -0
- package/src/ld/getLinkedDataContext.ts +80 -0
- package/tsconfig.json +27 -0
- package/types/credentials-context.d.ts +14 -0
- package/types/security-context.d.ts +6 -0
- package/examples/Status.yaml +0 -3
- package/examples/createAccountCredential.js +0 -27
- package/examples/createMineSweeperScoreCredential.js +0 -63
- package/examples/index.js +0 -9
- package/src/CredentialFactory.js +0 -67
- package/src/CredentialFactory.spec.js +0 -131
- package/src/Schema.js +0 -104
- package/src/Schema.spec.js +0 -172
- package/src/ValidationError.js +0 -31
- package/src/Validator.js +0 -128
- package/src/Validator.spec.js +0 -355
- package/src/helpers/cleanupAttributes.js +0 -71
- package/src/helpers/cleanupNulls.js +0 -42
- package/src/helpers/getReferenceIds.js +0 -71
- package/src/helpers/mapObject.js +0 -65
- package/src/helpers/normalizeAttributes.js +0 -28
- package/src/helpers/normalizeProperties.js +0 -61
- package/src/helpers/normalizeRequired.js +0 -37
- package/src/helpers/normalizeType.js +0 -41
- package/src/helpers/nullifyEmptyValues.js +0 -57
- package/src/helpers/removeRequiredAndDefault.js +0 -30
- package/src/helpers/validateId.js +0 -19
- package/src/index.d.ts +0 -25
- package/src/index.js +0 -8
- package/src/ld/documentLoader.js +0 -25
- package/src/ld/documentLoader.spec.js +0 -12
- package/src/ld/getLinkedDataContext.js +0 -63
- package/src/ld/getLinkedDataType.js +0 -38
- /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
- /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
|
@@ -0,0 +1,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;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { get } from 'lodash';
|
|
2
|
+
|
|
3
|
+
export type PropertySchema = {
|
|
4
|
+
'@type'?: string;
|
|
5
|
+
$ref?: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
format?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Returns linked data attribute type for a property schema, unless @type is defined */
|
|
11
|
+
const getLinkedDataAttributeType = (propertySchema: PropertySchema): string | undefined => {
|
|
12
|
+
const isOverriden = !!get(propertySchema, '@type');
|
|
13
|
+
|
|
14
|
+
if (isOverriden) {
|
|
15
|
+
return get(propertySchema, '@type');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// TODO: Add support for all types and formats, extend schema library with
|
|
19
|
+
// support for additional formats, e.g. URL, Duration etc.
|
|
20
|
+
const { type, format } = propertySchema;
|
|
21
|
+
|
|
22
|
+
const isDate = format === 'date';
|
|
23
|
+
const isNumber = type === 'number';
|
|
24
|
+
const isInteger = type === 'integer';
|
|
25
|
+
const isDateTime = format === 'date-time';
|
|
26
|
+
|
|
27
|
+
if (isInteger) {
|
|
28
|
+
return 'schema:Integer';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isNumber) {
|
|
32
|
+
return 'schema:Number';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isDate) {
|
|
36
|
+
return 'schema:Date';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isDateTime) {
|
|
40
|
+
return 'schema:DateTime';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default getLinkedDataAttributeType;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import getLinkedDataAttributeType, { type PropertySchema } from './getLinkedDataAttributeType';
|
|
2
|
+
|
|
3
|
+
export type { PropertySchema };
|
|
4
|
+
|
|
5
|
+
const SCHEMA_ORG_URI = 'https://schema.org/';
|
|
6
|
+
|
|
7
|
+
const PROTECTED_PROPERTIES = [
|
|
8
|
+
'id',
|
|
9
|
+
'type',
|
|
10
|
+
'schema'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
type ContextProperty = {
|
|
14
|
+
'@id': string;
|
|
15
|
+
'@type'?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ContextHeader = {
|
|
19
|
+
'@vocab': string;
|
|
20
|
+
'@version': number;
|
|
21
|
+
'@protected': boolean;
|
|
22
|
+
schema?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type LinkedDataContext = ContextHeader & {
|
|
26
|
+
[key: string]: ContextProperty | string | number | boolean | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Returns linked data context for a properties object */
|
|
30
|
+
const getLinkedDataContext = (properties: Record<string, PropertySchema>, vocabUri: string): LinkedDataContext => {
|
|
31
|
+
const context = {} as Record<string, ContextProperty>;
|
|
32
|
+
|
|
33
|
+
// TODO: Add support for embedded object, array and enum.
|
|
34
|
+
for (const key in properties) {
|
|
35
|
+
const propertySchema = properties[key];
|
|
36
|
+
|
|
37
|
+
const isProtected =
|
|
38
|
+
PROTECTED_PROPERTIES.includes(key) ||
|
|
39
|
+
key.startsWith('@');
|
|
40
|
+
|
|
41
|
+
if (isProtected) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
context[key] = { '@id': key };
|
|
46
|
+
|
|
47
|
+
const { $ref } = propertySchema;
|
|
48
|
+
|
|
49
|
+
if ($ref) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const linkedDataType = getLinkedDataAttributeType(propertySchema);
|
|
54
|
+
|
|
55
|
+
if (linkedDataType) {
|
|
56
|
+
context[key]['@type'] = linkedDataType;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const vocab = vocabUri.endsWith('/') || vocabUri.endsWith('#') ? vocabUri : `${vocabUri}#`;
|
|
61
|
+
|
|
62
|
+
const contextHeader = {
|
|
63
|
+
'@vocab': vocab,
|
|
64
|
+
'@version': 1.1,
|
|
65
|
+
'@protected': true
|
|
66
|
+
} as ContextHeader;
|
|
67
|
+
|
|
68
|
+
const isSchemaOrgDomain = vocab === SCHEMA_ORG_URI;
|
|
69
|
+
|
|
70
|
+
if (!isSchemaOrgDomain) {
|
|
71
|
+
contextHeader.schema = SCHEMA_ORG_URI;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...contextHeader,
|
|
76
|
+
...context
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default getLinkedDataContext;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "./src",
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"module": "nodenext",
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strictNullChecks": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noUnusedParameters": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"src",
|
|
20
|
+
"types"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist",
|
|
25
|
+
"**/*.test.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare module 'credentials-context' {
|
|
2
|
+
/**
|
|
3
|
+
* Map of credentials context URLs to their JSON-LD context documents
|
|
4
|
+
*/
|
|
5
|
+
export const contexts: Map<string, Record<string, unknown>>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Constants for credentials context
|
|
9
|
+
*/
|
|
10
|
+
export const constants: {
|
|
11
|
+
CREDENTIALS_CONTEXT_V1_URL: string;
|
|
12
|
+
[key: string]: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
package/examples/Status.yaml
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Schema, CredentialFactory } = require('../src')
|
|
4
|
-
|
|
5
|
-
const accountSchema = new Schema({
|
|
6
|
-
id: {},
|
|
7
|
-
username: { required: true },
|
|
8
|
-
createdAt: { format: 'date-time', required: true },
|
|
9
|
-
dateOfBirth: { format: 'date' }
|
|
10
|
-
}, 'Account')
|
|
11
|
-
|
|
12
|
-
const factory = new CredentialFactory('https://example.com/schema/AccountV1', accountSchema)
|
|
13
|
-
|
|
14
|
-
const createAccountCredential = (holder, username) => {
|
|
15
|
-
const id = `https://example.com/account/${username}`
|
|
16
|
-
|
|
17
|
-
const createdAt = new Date().toISOString()
|
|
18
|
-
const subject = {
|
|
19
|
-
id: holder,
|
|
20
|
-
username,
|
|
21
|
-
createdAt
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return factory.createCredential(id, holder, subject)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
module.exports = createAccountCredential
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Schema, CredentialFactory } = require('../src')
|
|
4
|
-
|
|
5
|
-
const GAME_NAME = 'MineSweeper'
|
|
6
|
-
const GAME_VERSION = '1.0'
|
|
7
|
-
|
|
8
|
-
const SCHEMA_ORG_URI = 'https://schema.org/'
|
|
9
|
-
|
|
10
|
-
const playerSchema = new Schema({
|
|
11
|
-
id: {},
|
|
12
|
-
hasVideoGameScore: { $ref: 'VideoGameScore', required: true }
|
|
13
|
-
}, 'Player')
|
|
14
|
-
|
|
15
|
-
const videoGameSchema = new Schema({
|
|
16
|
-
id: {},
|
|
17
|
-
name: { type: 'string', required: true },
|
|
18
|
-
version: { type: 'string', required: true }
|
|
19
|
-
}, 'VideoGame', SCHEMA_ORG_URI)
|
|
20
|
-
|
|
21
|
-
const videoGameScoreSchema = new Schema({
|
|
22
|
-
game: { $ref: 'VideoGame', required: true },
|
|
23
|
-
wins: { type: 'integer', required: true },
|
|
24
|
-
losses: { type: 'integer', required: true },
|
|
25
|
-
winRate: { type: 'number', required: true },
|
|
26
|
-
bestScore: { type: 'integer', required: true },
|
|
27
|
-
// TODO: Add duration as format:
|
|
28
|
-
endurance: { type: 'string', required: true, '@type': 'schema:Duration' },
|
|
29
|
-
dateCreated: { type: 'string', 'format': 'date-time', required: true },
|
|
30
|
-
bestRoundTime: { type: 'integer', required: true }
|
|
31
|
-
// difficultyLevel:
|
|
32
|
-
// enum:
|
|
33
|
-
// - EASY
|
|
34
|
-
// - MEDIUM
|
|
35
|
-
// - HARD
|
|
36
|
-
// required: true
|
|
37
|
-
}, 'VideoGameScore')
|
|
38
|
-
|
|
39
|
-
const CREDENTIAL_URI = `https://example.com/schema/${GAME_NAME}ScoreV1`
|
|
40
|
-
const SCHEMAS = [ playerSchema, videoGameSchema, videoGameScoreSchema ]
|
|
41
|
-
const factory = new CredentialFactory(CREDENTIAL_URI, SCHEMAS)
|
|
42
|
-
|
|
43
|
-
const createMineSweeperScoreCredential = (gameId, playerId, playerScore) => {
|
|
44
|
-
const credentialId = 'https://example.com/credentials/CREDENTIAL_ID'
|
|
45
|
-
|
|
46
|
-
const game = {
|
|
47
|
-
id: gameId,
|
|
48
|
-
name: GAME_NAME,
|
|
49
|
-
version: GAME_VERSION
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const player = {
|
|
53
|
-
id: playerId,
|
|
54
|
-
hasVideoGameScore: {
|
|
55
|
-
...playerScore,
|
|
56
|
-
game
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return factory.createCredential(credentialId, playerId, player)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
module.exports = createMineSweeperScoreCredential
|
package/examples/index.js
DELETED