);
18 | };
19 |
20 | export default PageNavigation;
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JSON:API Explorer
2 | The JSON:API Explorer is an interactive web application for exploring JSON:API
3 | servers. [Try it!](https://explore.jsonapi.dev)
4 |
5 | **Caveat**: this project and the example server behind it are using non-standard
6 | features including:
7 | - JSON Schema
8 | - An unprofiled filter syntax
9 | - A "home document" at `/jsonapi`
10 |
11 | Over time, we hope these features will be validated, polished and made part of
12 | the official standard.
13 |
14 | ## Contributing
15 |
16 | We're looking for help of all kinds!
17 |
18 | Particularly:
19 |
20 | - We want it to be beautiful
21 | - We want it to be intuitive
22 | - We want it to support alternative filtering strategies via profiles
23 | - We want its use of JSON Schema to be standardized via a profile or the base spec
24 |
25 | But most of all:
26 |
27 | - We want you to love it
28 |
29 | So, please, feel free to make open issues to discuss potential improvements and
30 | then open PRs to make them.
31 |
32 | ## Set
33 |
34 | 1. Run `npm install`
35 | 2. Run `npm run start` to activate the development server.
36 |
37 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { Location } from './contexts/location';
4 | import ExplorerUI from './components/explorer-ui';
5 | import LocationBar from './components/location-ui';
6 | import AppTitle from './components/app-title';
7 | import FieldFocus from './contexts/field-focus';
8 |
9 | const App = ({options}) => {
10 | const { exploredUrl } = options;
11 | const initialLandingUrl = exploredUrl || new URL(document.location.href).searchParams.get('location') || '';
12 | const [landingUrl, setLandingUrl] = useState(initialLandingUrl);
13 |
14 | return (
15 |
);
141 | };
142 |
143 | export default Summary;
144 |
--------------------------------------------------------------------------------
/src/lib/schema/schema-parser.js:
--------------------------------------------------------------------------------
1 | import $RefParser from 'json-schema-ref-parser';
2 | import { getAttributes, getRelationships } from './normalize';
3 | import { request } from '../../utils/request';
4 | import { extract } from '../../utils';
5 | import Document from '../jsonapi-objects/document';
6 | import { compileJsonApiUrl, parseJsonApiUrl } from '../url/url';
7 |
8 | export default class SchemaParser {
9 | constructor() {
10 | this.schemaCache = {};
11 | this.inferenceCache = {};
12 | }
13 |
14 | async parse(root, forPath = []) {
15 | if (typeof root === 'string') {
16 | return this.loadSchema(root).then(schema =>
17 | this.parseSchema(schema, forPath),
18 | );
19 | }
20 | const links = root.getLinks();
21 | const describedByURL = extract(links, 'describedby.href');
22 | if (describedByURL) {
23 | return this.parseSchema(await this.loadSchema(describedByURL), forPath);
24 | }
25 | const selfURL = extract(links, 'self.href');
26 | const parsedSelfURL = parseJsonApiUrl(selfURL);
27 | if (Object.keys(parsedSelfURL.query.fields).length) {
28 | const completeFieldsetUrl = Object.assign({}, parsedSelfURL, {
29 | query: Object.assign({}, parsedSelfURL.query, { fields: [] }),
30 | });
31 | return this.parse(
32 | Document.parse(await request(compileJsonApiUrl(completeFieldsetUrl))),
33 | forPath,
34 | );
35 | }
36 | const baseResourceURL = compileJsonApiUrl(
37 | Object.assign({}, parsedSelfURL, { protocol: 'inferred:', query: {} }),
38 | );
39 | const inferredSchema = this.inferSchema(root, forPath);
40 | return !forPath.length
41 | ? this.mergeWithCachedInference(baseResourceURL, inferredSchema)
42 | : inferredSchema;
43 | }
44 |
45 | parseSchema(schema, forPath) {
46 | const dataSchema = extract(schema, 'definitions.data');
47 | const relationships = getRelationships(dataSchema);
48 | const discovered = {
49 | title: extract(schema, 'title'),
50 | type: dataSchema ? extract(dataSchema, (dataSchema.type === 'array' ? 'items.' : '') + 'definitions.type.const') : undefined,
51 | attributes: getAttributes(dataSchema),
52 | relationships,
53 | links: extract(extract(schema, 'allOf', [{}, {}])[1], 'links', []),
54 | };
55 | if (forPath.length) {
56 | const [next, ...further] = forPath;
57 | const targetSchema = relationships.find(obj => obj.name === next).value;
58 | return targetSchema ? this.parse(targetSchema, further) : null;
59 | } else {
60 | return discovered;
61 | }
62 | }
63 |
64 | inferSchema(responseDocument, forPath) {
65 | if (responseDocument.isEmptyDocument()) {
66 | return null;
67 | }
68 | let inferred;
69 | if (forPath.length) {
70 | const [next, ...further] = forPath;
71 | const documentData = [responseDocument.getData()]
72 | .flat()
73 | .reduce((grouped, resourceObject) => {
74 | return Object.assign(grouped, {
75 | [resourceObject.getType()]: [
76 | ...(grouped[resourceObject.getType()] || []),
77 | resourceObject,
78 | ],
79 | });
80 | }, {});
81 | inferred = Object.entries(documentData)
82 | .flatMap(([type, groupedData]) => {
83 | const relatedData = groupedData
84 | .flatMap(resourceObject => {
85 | return resourceObject.getRelated(next);
86 | })
87 | .reduce((raw, item) => raw.concat(item ? [item.raw] : []), []);
88 | const included = responseDocument.getIncluded().map(item => item.raw);
89 | const syntheticDocument = Document.parse({
90 | data: relatedData,
91 | included,
92 | });
93 | return this.mergeWithCachedInference(
94 | `${type}/${further.join('/')}`,
95 | this.inferSchema(syntheticDocument, further),
96 | );
97 | })
98 | .reduce(this.mergeResourceObjectSchema);
99 | } else {
100 | [responseDocument.getData()].flat().forEach(item => {
101 | inferred = this.buildInferenceFromResourceObject(item);
102 | });
103 | }
104 | return inferred;
105 | }
106 |
107 | buildInferenceFromResourceObject(resourceObject) {
108 | const type = resourceObject.getType();
109 |
110 | const inference = {
111 | type,
112 | attributes: Object.keys(resourceObject.getAttributes()).map(name => {
113 | return { name };
114 | }),
115 | relationships: Object.keys(resourceObject.getRelationships()).map(
116 | name => {
117 | return { name };
118 | },
119 | ),
120 | };
121 |
122 | return this.mergeWithCachedInference(inference.type, inference);
123 | }
124 |
125 | mergeWithCachedInference(key, inference) {
126 | if (!inference) {
127 | return this.inferenceCache[key] || inference;
128 | }
129 | this.inferenceCache[key] = this.mergeResourceObjectSchema(
130 | inference,
131 | this.inferenceCache[key] || {
132 | type: inference.type,
133 | attributes: [],
134 | relationships: [],
135 | },
136 | );
137 | return this.inferenceCache[key];
138 | }
139 |
140 | mergeResourceObjectSchema(schema, otherSchema) {
141 | if (schema.type !== otherSchema.type) {
142 | return schema;
143 | }
144 |
145 | const mergeFields = (merged, otherField) =>
146 | merged.concat(
147 | merged.some(knownField => knownField.name === otherField.name)
148 | ? []
149 | : [otherField],
150 | );
151 |
152 | const mergedSchema = {
153 | type: schema.type,
154 | attributes: [],
155 | relationships: [],
156 | };
157 | mergedSchema.attributes.push(
158 | ...schema.attributes.reduce(mergeFields, otherSchema.attributes),
159 | );
160 | mergedSchema.relationships.push(
161 | ...schema.relationships.reduce(mergeFields, otherSchema.relationships),
162 | );
163 |
164 | return mergedSchema;
165 | }
166 |
167 | loadSchema(schemaId) {
168 | let schemaPromise;
169 | if (!this.schemaCache.hasOwnProperty(schemaId)) {
170 | const publish = (success, result) =>
171 | this.schemaCache[schemaId].forEach(([resolve, reject]) =>
172 | success ? resolve(result) : reject(result),
173 | );
174 | $RefParser
175 | .dereference(schemaId)
176 | .then(result => {
177 | publish(true, result);
178 | this.schemaCache[schemaId] = result;
179 | })
180 | .catch(result => publish(false, result));
181 | }
182 | if (
183 | !this.schemaCache.hasOwnProperty(schemaId) ||
184 | Array.isArray(this.schemaCache[schemaId])
185 | ) {
186 | schemaPromise = new Promise(
187 | (resolve, reject) =>
188 | (this.schemaCache[schemaId] = [
189 | ...(this.schemaCache[schemaId] || []),
190 | [resolve, reject],
191 | ]),
192 | );
193 | } else {
194 | schemaPromise = Promise.resolve(this.schemaCache[schemaId]);
195 | }
196 | return schemaPromise;
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/lib/url/url.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | parseJsonApiUrl,
3 | compileJsonApiUrl,
4 | compileQueryParameterFamily,
5 | } from './url';
6 |
7 | const baseUrl = 'http://drupal.test/jsonapi';
8 | const articleUrl = `${baseUrl}/node/article`;
9 |
10 | const local = {
11 | url: 'http://127.0.0.1:8080/',
12 | parsed: {
13 | protocol: 'http:',
14 | host: '127.0.0.1:8080',
15 | port: '8080',
16 | path: '/',
17 | query: {
18 | filter: {},
19 | include: [],
20 | fields: {},
21 | page: {},
22 | sort: [],
23 | },
24 | fragment: '',
25 | }
26 | }
27 |
28 | const base = {
29 | url: 'http://drupal.test/jsonapi',
30 | parsed: {
31 | protocol: 'http:',
32 | host: 'drupal.test',
33 | port: '',
34 | path: '/jsonapi',
35 | query: {
36 | filter: {},
37 | include: [],
38 | fields: {},
39 | page: {},
40 | sort: [],
41 | },
42 | fragment: '',
43 | },
44 | };
45 |
46 | const article = {
47 | url: `${base.url}/node/article`,
48 | parsed: {
49 | protocol: 'http:',
50 | host: 'drupal.test',
51 | port: '',
52 | path: '/jsonapi/node/article',
53 | query: {
54 | filter: {},
55 | include: [],
56 | fields: {},
57 | page: {},
58 | sort: [],
59 | },
60 | fragment: '',
61 | },
62 | };
63 |
64 | const include = {
65 | urls: ['include=uid', 'include=uid,node_type'],
66 | parsed: [
67 | {
68 | protocol: 'http:',
69 | host: 'drupal.test',
70 | port: '',
71 | path: '/jsonapi/node/article',
72 | query: {
73 | filter: {},
74 | include: ['uid'],
75 | fields: {},
76 | page: {},
77 | sort: [],
78 | },
79 | fragment: '',
80 | },
81 | {
82 | protocol: 'http:',
83 | host: 'drupal.test',
84 | port: '',
85 | path: '/jsonapi/node/article',
86 | query: {
87 | filter: {},
88 | include: ['uid', 'node_type'],
89 | fields: {},
90 | page: {},
91 | sort: [],
92 | },
93 | fragment: '',
94 | },
95 | ],
96 | };
97 |
98 | const fields = {
99 | urls: [
100 | 'fields[node--article]=drupal_internal__nid',
101 | 'fields[node--article]=drupal_internal__nid,status',
102 | 'fields[node--article]=drupal_internal__nid&fields[user_role--user_role]=drupal_internal__id',
103 | ],
104 | parsed: [
105 | {
106 | protocol: 'http:',
107 | host: 'drupal.test',
108 | port: '',
109 | path: '/jsonapi/node/article',
110 | query: {
111 | filter: {},
112 | include: [],
113 | fields: {
114 | 'node--article': new Set(['drupal_internal__nid']),
115 | },
116 | page: {},
117 | sort: [],
118 | },
119 | fragment: '',
120 | },
121 | {
122 | protocol: 'http:',
123 | host: 'drupal.test',
124 | port: '',
125 | path: '/jsonapi/node/article',
126 | query: {
127 | filter: {},
128 | include: [],
129 | fields: {
130 | 'node--article': new Set(['drupal_internal__nid', 'status']),
131 | },
132 | page: {},
133 | sort: [],
134 | },
135 | fragment: '',
136 | },
137 | {
138 | protocol: 'http:',
139 | host: 'drupal.test',
140 | port: '',
141 | path: '/jsonapi/node/article',
142 | query: {
143 | filter: {},
144 | include: [],
145 | fields: {
146 | 'node--article': new Set(['drupal_internal__nid']),
147 | 'user_role--user_role': new Set(['drupal_internal__id']),
148 | },
149 | page: {},
150 | sort: [],
151 | },
152 | fragment: '',
153 | },
154 | ],
155 | };
156 |
157 | const filters = {
158 | urls: [
159 | 'filter[foo]=bar',
160 | 'filter[foo][bar]=baz',
161 | 'filter[foo][]=bar&filter[foo][]=baz',
162 | 'filter[foo][bar]=qux&filter[foo][baz]=quux',
163 | 'filter[foo][bar][]=baz&filter[foo][bar][]=qux',
164 | 'filter[foo][bar][]=qux&filter[foo][bar][]=quux&filter[foo][baz][]=quz&filter[foo][baz][]=quuz',
165 | 'filter[a-label][condition][path]=field_first_name&filter[a-label][condition][operator]=%3D&filter[a-label][condition][value]=Janis',
166 | 'filter[name-filter][condition][path]=uid.name&filter[name-filter][condition][operator]=IN&filter[name-filter][condition][value][]=admin&filter[name-filter][condition][value][]=john',
167 | ],
168 |
169 | parsed: [
170 | {
171 | protocol: 'http:',
172 | host: 'drupal.test',
173 | port: '',
174 | path: '/jsonapi/node/article',
175 | query: {
176 | filter: { foo: 'bar' },
177 | include: [],
178 | fields: {},
179 | page: {},
180 | sort: [],
181 | },
182 | fragment: '',
183 | },
184 | {
185 | protocol: 'http:',
186 | host: 'drupal.test',
187 | port: '',
188 | path: '/jsonapi/node/article',
189 | query: {
190 | filter: { foo: { bar: 'baz' } },
191 | include: [],
192 | fields: {},
193 | page: {},
194 | sort: [],
195 | },
196 | fragment: '',
197 | },
198 | {
199 | protocol: 'http:',
200 | host: 'drupal.test',
201 | port: '',
202 | path: '/jsonapi/node/article',
203 | query: {
204 | filter: { foo: new Set(['bar', 'baz']) },
205 | include: [],
206 | fields: {},
207 | page: {},
208 | sort: [],
209 | },
210 | fragment: '',
211 | },
212 | {
213 | protocol: 'http:',
214 | host: 'drupal.test',
215 | port: '',
216 | path: '/jsonapi/node/article',
217 | query: {
218 | filter: { foo: { bar: 'qux', baz: 'quux' } },
219 | include: [],
220 | fields: {},
221 | page: {},
222 | sort: [],
223 | },
224 | fragment: '',
225 | },
226 | {
227 | protocol: 'http:',
228 | host: 'drupal.test',
229 | port: '',
230 | path: '/jsonapi/node/article',
231 | query: {
232 | filter: { foo: { bar: new Set(['baz', 'qux']) } },
233 | include: [],
234 | fields: {},
235 | page: {},
236 | sort: [],
237 | },
238 | fragment: '',
239 | },
240 | {
241 | protocol: 'http:',
242 | host: 'drupal.test',
243 | port: '',
244 | path: '/jsonapi/node/article',
245 | query: {
246 | filter: {
247 | foo: {
248 | bar: new Set(['qux', 'quux']),
249 | baz: new Set(['quz', 'quuz']),
250 | },
251 | },
252 | include: [],
253 | fields: {},
254 | page: {},
255 | sort: [],
256 | },
257 | fragment: '',
258 | },
259 | {
260 | protocol: 'http:',
261 | host: 'drupal.test',
262 | port: '',
263 | path: '/jsonapi/node/article',
264 | query: {
265 | filter: {
266 | 'a-label': {
267 | condition: {
268 | path: 'field_first_name',
269 | operator: '=',
270 | value: 'Janis',
271 | },
272 | },
273 | },
274 | include: [],
275 | fields: {},
276 | page: {},
277 | sort: [],
278 | },
279 | fragment: '',
280 | },
281 | {
282 | protocol: 'http:',
283 | host: 'drupal.test',
284 | port: '',
285 | path: '/jsonapi/node/article',
286 | query: {
287 | filter: {
288 | 'name-filter': {
289 | condition: {
290 | path: 'uid.name',
291 | operator: 'IN',
292 | value: new Set(['admin', 'john']),
293 | },
294 | },
295 | },
296 | include: [],
297 | fields: {},
298 | page: {},
299 | sort: [],
300 | },
301 | fragment: '',
302 | },
303 | ],
304 | };
305 |
306 | const complex = {
307 | urls: [
308 | [
309 | 'include=node_type,uid.roles',
310 | 'filter[last-name-filter][condition][path]=field_last_name',
311 | 'filter[last-name-filter][condition][operator]=STARTS_WITH',
312 | 'filter[last-name-filter][condition][value]=J',
313 | 'fields[node--article]=drupal_internal__nid',
314 | 'fields[node_type--node_type]=drupal_internal__type,name',
315 | 'fields[user_role--user_role]=drupal_internal__id',
316 | ],
317 | ],
318 | parsed: [
319 | {
320 | protocol: 'http:',
321 | host: 'drupal.test',
322 | port: '',
323 | path: '/jsonapi/node/article',
324 | query: {
325 | filter: {
326 | 'last-name-filter': {
327 | condition: {
328 | path: 'field_last_name',
329 | operator: 'STARTS_WITH',
330 | value: 'J',
331 | },
332 | },
333 | },
334 | include: ['node_type', 'uid.roles'],
335 | fields: {
336 | 'node--article': new Set(['drupal_internal__nid']),
337 | 'node_type--node_type': new Set(['drupal_internal__type', 'name']),
338 | 'user_role--user_role': new Set(['drupal_internal__id']),
339 | },
340 | page: {},
341 | sort: [],
342 | },
343 | fragment: '',
344 | },
345 | ],
346 | };
347 |
348 | describe('Parse JSON:API url from url string', () => {
349 | test('Top Level url', () => {
350 | expect(parseJsonApiUrl(base.url)).toEqual(base.parsed);
351 | expect(parseJsonApiUrl(local.url)).toEqual(local.parsed);
352 | });
353 |
354 | test('Collection url', () => {
355 | expect(parseJsonApiUrl(article.url)).toEqual(article.parsed);
356 | });
357 |
358 | test('With Include', () => {
359 | include.urls.forEach((url, index) => {
360 | expect(parseJsonApiUrl(`${article.url}?${url}`)).toEqual(
361 | include.parsed[index],
362 | );
363 | });
364 | });
365 |
366 | test('With Fields', () => {
367 | fields.urls.forEach((url, index) => {
368 | expect(parseJsonApiUrl(`${article.url}?${url}`)).toEqual(
369 | fields.parsed[index],
370 | );
371 | });
372 | });
373 |
374 | test('With Filters', () => {
375 | filters.urls.forEach((url, index) => {
376 | expect(parseJsonApiUrl(`${article.url}?${url}`)).toEqual(
377 | filters.parsed[index],
378 | );
379 | });
380 | });
381 |
382 | test('Complex url with fields and include', () => {
383 | complex.urls.forEach((url, index) => {
384 | expect(parseJsonApiUrl(`${article.url}?${url.join('&')}`)).toEqual(
385 | complex.parsed[index],
386 | );
387 | });
388 | });
389 | });
390 |
391 | describe('Compile url from JSON:API url object', () => {
392 | test('Top level url', () => {
393 | expect(compileJsonApiUrl(base.parsed)).toBe(base.url);
394 | expect(compileJsonApiUrl(local.parsed)).toBe(local.url);
395 | });
396 |
397 | test('Collection url', () => {
398 | expect(compileJsonApiUrl(article.parsed)).toBe(article.url);
399 | });
400 |
401 | test('With Include', () => {
402 | include.urls.forEach((url, index) => {
403 | expect(compileJsonApiUrl(include.parsed[index])).toEqual(
404 | `${article.url}?${url}`,
405 | );
406 | });
407 | });
408 |
409 | test('With Fields', () => {
410 | fields.urls.forEach((url, index) => {
411 | expect(compileJsonApiUrl(fields.parsed[index])).toEqual(
412 | `${article.url}?${url}`,
413 | );
414 | });
415 | });
416 |
417 | test('With Filters', () => {
418 | filters.urls.forEach((url, index) => {
419 | expect(compileJsonApiUrl(filters.parsed[index])).toEqual(
420 | `${article.url}?${url}`,
421 | );
422 | });
423 | });
424 |
425 | test('Complex url', () => {
426 | complex.urls.forEach((url, index) => {
427 | expect(compileJsonApiUrl(complex.parsed[index])).toEqual(
428 | `${article.url}?${url.join('&')}`,
429 | );
430 | });
431 | });
432 | });
433 |
434 | describe('Compile filter query', () => {
435 | filters.urls.forEach((url, index) => {
436 | expect(
437 | compileQueryParameterFamily('filter', filters.parsed[index].query.filter),
438 | ).toBe(url);
439 | });
440 | });
441 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/src/lib/schema/normalize.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | getAttributes,
3 | getRelationships,
4 | getRelationshipSchema,
5 | mapDefinitions,
6 | } from './normalize';
7 |
8 | let schemaUnd;
9 |
10 | const emptyVals = [[], {}, null, schemaUnd];
11 |
12 | const schemaMenu = {
13 | $schema: 'http://json-schema.org/draft-07/schema',
14 | $id: 'http://drupal.test/jsonapi/menu/menu/resource/schema.json',
15 | allOf: [
16 | {
17 | type: 'object',
18 | properties: {
19 | attributes: {
20 | $ref: '#/definitions/attributes',
21 | },
22 | relationships: {
23 | $ref: '#/definitions/relationships',
24 | },
25 | },
26 | },
27 | {
28 | $ref: 'https://jsonapi.org/schema#/definitions/resource',
29 | },
30 | ],
31 | properties: {
32 | attributes: {
33 | $ref: '#/definitions/attributes',
34 | },
35 | },
36 | definitions: {
37 | attributes: {
38 | type: 'object',
39 | properties: {
40 | drupal_internal__id: {},
41 | langcode: {},
42 | status: {},
43 | dependencies: {},
44 | third_party_settings: {},
45 | label: {},
46 | description: {},
47 | locked: {},
48 | },
49 | additionalProperties: false,
50 | },
51 | },
52 | };
53 |
54 | const schemaArticleCollection = {
55 | $schema: 'http://json-schema.org/draft-07/schema',
56 | $id: 'http://drupal.test/jsonapi/node/article/collection/schema.json',
57 | allOf: [
58 | {
59 | $ref: 'https://jsonapi.org/schema',
60 | },
61 | {
62 | if: {
63 | $ref: 'https://jsonapi.org/schema#/definitions/success',
64 | },
65 | then: {
66 | type: 'object',
67 | properties: {
68 | data: {
69 | $ref: '#/definitions/data',
70 | },
71 | },
72 | required: ['data'],
73 | },
74 | },
75 | ],
76 | definitions: {
77 | data: {
78 | type: 'array',
79 | items: {
80 | $ref: 'http://drupal.test/jsonapi/node/article/resource/schema.json',
81 | },
82 | },
83 | },
84 | };
85 |
86 | const schemaNodeTypeRelated = {
87 | $schema: 'http://json-schema.org/draft-07/schema',
88 | $id:
89 | 'http://drupal.test/jsonapi/node/article/resource/relationships/node_type/related/schema.json',
90 | allOf: [
91 | {
92 | $ref: 'https://jsonapi.org/schema',
93 | },
94 | {
95 | if: {
96 | $ref: 'https://jsonapi.org/schema#/definitions/success',
97 | },
98 | then: {
99 | type: 'object',
100 | properties: {
101 | data: {
102 | $ref: '#/definitions/data',
103 | },
104 | },
105 | required: ['data'],
106 | },
107 | },
108 | ],
109 | definitions: {
110 | data: {
111 | $ref:
112 | 'http://drupal.test/jsonapi/node_type/node_type/resource/schema.json',
113 | },
114 | },
115 | };
116 |
117 | const schemaArticle = {
118 | "$schema": "https://json-schema.org/draft/2019-09/hyper-schema",
119 | "$id": "http://drupal.test/jsonapi/node/article/resource/schema",
120 | "title": "Article content item",
121 | "allOf": [
122 | {
123 | "type": "object",
124 | "properties": {
125 | "type": {
126 | "$ref": "#definitions/type"
127 | },
128 | "attributes": {
129 | "$ref": "#/definitions/attributes"
130 | },
131 | "relationships": {
132 | "$ref": "#/definitions/relationships"
133 | }
134 | }
135 | },
136 | {
137 | "$ref": "https://jsonapi.org/schema#/definitions/resource"
138 | }
139 | ],
140 | "definitions": {
141 | "type": {
142 | "const": "node--article"
143 | },
144 | "attributes": {
145 | "description": "Entity attributes",
146 | "type": "object",
147 | "properties": {
148 | "uuid": {
149 | "type": "string",
150 | "title": "UUID",
151 | "maxLength": 128
152 | },
153 | "drupal_internal__nid": {
154 | "type": "integer",
155 | "title": "ID"
156 | },
157 | "drupal_internal__vid": {
158 | "type": "integer",
159 | "title": "Revision ID"
160 | },
161 | "langcode": {
162 | "type": "string",
163 | "title": "Language"
164 | },
165 | "revision_timestamp": {
166 | "type": "number",
167 | "title": "Revision create time",
168 | "format": "utc-millisec",
169 | "description": "The time that the current revision was created."
170 | },
171 | "revision_log": {
172 | "type": "string",
173 | "title": "Revision log message",
174 | "description": "Briefly describe the changes you have made.",
175 | "default": ""
176 | },
177 | "status": {
178 | "type": "boolean",
179 | "title": "Published",
180 | "default": true
181 | },
182 | "title": {
183 | "type": "string",
184 | "title": "Title",
185 | "maxLength": 255
186 | },
187 | "created": {
188 | "type": "number",
189 | "title": "Authored on",
190 | "format": "utc-millisec",
191 | "description": "The time that the node was created."
192 | },
193 | "changed": {
194 | "type": "number",
195 | "title": "Changed",
196 | "format": "utc-millisec",
197 | "description": "The time that the node was last edited."
198 | },
199 | "promote": {
200 | "type": "boolean",
201 | "title": "Promoted to front page",
202 | "default": true
203 | },
204 | "sticky": {
205 | "type": "boolean",
206 | "title": "Sticky at top of lists",
207 | "default": false
208 | },
209 | "default_langcode": {
210 | "type": "boolean",
211 | "title": "Default translation",
212 | "description": "A flag indicating whether this is the default translation.",
213 | "default": true
214 | },
215 | "revision_default": {
216 | "type": "boolean",
217 | "title": "Default revision",
218 | "description": "A flag indicating whether this was a default revision when it was saved."
219 | },
220 | "revision_translation_affected": {
221 | "type": "boolean",
222 | "title": "Revision translation affected",
223 | "description": "Indicates if the last edit of a translation belongs to current revision."
224 | },
225 | "path": {
226 | "type": "object",
227 | "properties": {
228 | "alias": {
229 | "type": "string",
230 | "title": "Path alias"
231 | },
232 | "pid": {
233 | "type": "integer",
234 | "title": "Path id"
235 | },
236 | "langcode": {
237 | "type": "string",
238 | "title": "Language Code"
239 | }
240 | },
241 | "title": "URL alias"
242 | },
243 | "body": {
244 | "type": "object",
245 | "properties": {
246 | "value": {
247 | "type": "string",
248 | "title": "Text"
249 | },
250 | "format": {
251 | "type": "string",
252 | "title": "Text format"
253 | },
254 | "summary": {
255 | "type": "string",
256 | "title": "Summary"
257 | }
258 | },
259 | "required": [
260 | "value"
261 | ],
262 | "title": "Body"
263 | },
264 | "comment": {
265 | "type": "object",
266 | "properties": {
267 | "status": {
268 | "type": "integer",
269 | "title": "Comment status"
270 | },
271 | "cid": {
272 | "type": "integer",
273 | "title": "Last comment ID"
274 | },
275 | "last_comment_timestamp": {
276 | "type": "integer",
277 | "title": "Last comment timestamp",
278 | "description": "The time that the last comment was created."
279 | },
280 | "last_comment_name": {
281 | "type": "string",
282 | "title": "Last comment name",
283 | "description": "The name of the user posting the last comment."
284 | },
285 | "last_comment_uid": {
286 | "type": "integer",
287 | "title": "Last comment user ID"
288 | },
289 | "comment_count": {
290 | "type": "integer",
291 | "title": "Number of comments",
292 | "description": "The number of comments."
293 | }
294 | },
295 | "required": [
296 | "status"
297 | ],
298 | "title": "Comments",
299 | "default": {
300 | "status": 2,
301 | "cid": 0,
302 | "last_comment_timestamp": 0,
303 | "last_comment_name": null,
304 | "last_comment_uid": 0,
305 | "comment_count": 0
306 | }
307 | },
308 | "field_multivalue": {
309 | "type": "array",
310 | "title": "multivalue",
311 | "items": {
312 | "type": "object",
313 | "properties": {
314 | "value": {
315 | "type": "string",
316 | "title": "Text",
317 | "maxLength": 255
318 | },
319 | "format": {
320 | "type": "string",
321 | "title": "Text format"
322 | }
323 | },
324 | "required": [
325 | "value"
326 | ]
327 | }
328 | },
329 | "field_number": {
330 | "type": "array",
331 | "title": "Number",
332 | "items": {
333 | "type": "integer",
334 | "title": "Integer value"
335 | }
336 | }
337 | },
338 | "required": [
339 | "uuid",
340 | "drupal_internal__nid",
341 | "drupal_internal__vid",
342 | "title",
343 | "revision_translation_affected",
344 | "path"
345 | ],
346 | "additionalProperties": false
347 | },
348 | "relationships": {
349 | "description": "Entity relationships",
350 | "properties": {
351 | "node_type": {
352 | "type": "object",
353 | "properties": {
354 | "data": {
355 | "type": "object",
356 | "required": [
357 | "type",
358 | "id"
359 | ],
360 | "properties": {
361 | "type": {
362 | "type": "string",
363 | "title": "Referenced resource",
364 | "enum": [
365 | "node_type--node_type"
366 | ]
367 | },
368 | "id": {
369 | "type": "string",
370 | "title": "Resource ID",
371 | "format": "uuid",
372 | "maxLength": 128
373 | },
374 | "meta": {
375 | "type": "string",
376 | "title": "Content type ID"
377 | }
378 | }
379 | }
380 | },
381 | "title": "Content type",
382 | "links": [
383 | {
384 | "href": "{instanceHref}",
385 | "rel": "related",
386 | "targetMediaType": "application/vnd.api+json",
387 | "targetSchema": "http://drupal.test/jsonapi/node/article/resource/relationships/node_type/related/schema",
388 | "templatePointers": {
389 | "instanceHref": "/links/related/href"
390 | },
391 | "templateRequired": [
392 | "instanceHref"
393 | ]
394 | }
395 | ]
396 | },
397 | "revision_uid": {
398 | "type": "object",
399 | "properties": {
400 | "data": {
401 | "type": "object",
402 | "required": [
403 | "type",
404 | "id"
405 | ],
406 | "properties": {
407 | "type": {
408 | "type": "string",
409 | "title": "Referenced resource",
410 | "enum": [
411 | "user--user"
412 | ]
413 | },
414 | "id": {
415 | "type": "string",
416 | "title": "Resource ID",
417 | "format": "uuid",
418 | "maxLength": 128
419 | },
420 | "meta": {
421 | "type": "integer",
422 | "title": "User ID"
423 | }
424 | }
425 | }
426 | },
427 | "title": "Revision user",
428 | "links": [
429 | {
430 | "href": "{instanceHref}",
431 | "rel": "related",
432 | "targetMediaType": "application/vnd.api+json",
433 | "targetSchema": "http://drupal.test/jsonapi/node/article/resource/relationships/revision_uid/related/schema",
434 | "templatePointers": {
435 | "instanceHref": "/links/related/href"
436 | },
437 | "templateRequired": [
438 | "instanceHref"
439 | ]
440 | }
441 | ]
442 | },
443 | "uid": {
444 | "type": "object",
445 | "properties": {
446 | "data": {
447 | "type": "object",
448 | "required": [
449 | "type",
450 | "id"
451 | ],
452 | "properties": {
453 | "type": {
454 | "type": "string",
455 | "title": "Referenced resource",
456 | "enum": [
457 | "user--user"
458 | ]
459 | },
460 | "id": {
461 | "type": "string",
462 | "title": "Resource ID",
463 | "format": "uuid",
464 | "maxLength": 128
465 | },
466 | "meta": {
467 | "type": "integer",
468 | "title": "User ID"
469 | }
470 | }
471 | }
472 | },
473 | "title": "Authored by",
474 | "links": [
475 | {
476 | "href": "{instanceHref}",
477 | "rel": "related",
478 | "targetMediaType": "application/vnd.api+json",
479 | "targetSchema": "http://drupal.test/jsonapi/node/article/resource/relationships/uid/related/schema",
480 | "templatePointers": {
481 | "instanceHref": "/links/related/href"
482 | },
483 | "templateRequired": [
484 | "instanceHref"
485 | ]
486 | }
487 | ]
488 | },
489 | "field_image": {
490 | "type": "object",
491 | "properties": {
492 | "data": {
493 | "type": "array",
494 | "items": {
495 | "type": "object",
496 | "required": [
497 | "type",
498 | "id"
499 | ],
500 | "properties": {
501 | "type": {
502 | "type": "string",
503 | "title": "Referenced resource",
504 | "enum": [
505 | "file--file"
506 | ]
507 | },
508 | "id": {
509 | "type": "string",
510 | "title": "Resource ID",
511 | "format": "uuid",
512 | "maxLength": 128
513 | },
514 | "meta": {
515 | "type": "object",
516 | "properties": {
517 | "target_id": {
518 | "type": "integer",
519 | "title": "File ID"
520 | },
521 | "alt": {
522 | "type": "string",
523 | "title": "Alternative text",
524 | "description": "Alternative image text, for the image\\'s \\'alt\\' attribute."
525 | },
526 | "title": {
527 | "type": "string",
528 | "title": "Title",
529 | "description": "Image title text, for the image\\'s \\'title\\' attribute."
530 | },
531 | "width": {
532 | "type": "integer",
533 | "title": "Width",
534 | "description": "The width of the image in pixels."
535 | },
536 | "height": {
537 | "type": "integer",
538 | "title": "Height",
539 | "description": "The height of the image in pixels."
540 | }
541 | },
542 | "required": [
543 | "target_id"
544 | ]
545 | }
546 | }
547 | }
548 | }
549 | },
550 | "title": "Image",
551 | "maxItems": 2,
552 | "links": [
553 | {
554 | "href": "{instanceHref}",
555 | "rel": "related",
556 | "targetMediaType": "application/vnd.api+json",
557 | "targetSchema": "http://drupal.test/jsonapi/node/article/resource/relationships/field_image/related/schema",
558 | "templatePointers": {
559 | "instanceHref": "/links/related/href"
560 | },
561 | "templateRequired": [
562 | "instanceHref"
563 | ]
564 | }
565 | ]
566 | },
567 | "field_tags": {
568 | "type": "object",
569 | "properties": {
570 | "data": {
571 | "type": "array",
572 | "items": {
573 | "type": "object",
574 | "required": [
575 | "type",
576 | "id"
577 | ],
578 | "properties": {
579 | "type": {
580 | "type": "string",
581 | "title": "Referenced resource",
582 | "enum": [
583 | "taxonomy_term--other",
584 | "taxonomy_term--tags"
585 | ]
586 | },
587 | "id": {
588 | "type": "string",
589 | "title": "Resource ID",
590 | "format": "uuid",
591 | "maxLength": 128
592 | },
593 | "meta": {
594 | "type": "integer",
595 | "title": "Taxonomy term ID"
596 | }
597 | }
598 | }
599 | }
600 | },
601 | "title": "Tags",
602 | "links": [
603 | {
604 | "href": "{instanceHref}",
605 | "rel": "related",
606 | "targetMediaType": "application/vnd.api+json",
607 | "targetSchema": "http://drupal.test/jsonapi/node/article/resource/relationships/field_tags/related/schema",
608 | "templatePointers": {
609 | "instanceHref": "/links/related/href"
610 | },
611 | "templateRequired": [
612 | "instanceHref"
613 | ]
614 | }
615 | ]
616 | }
617 | },
618 | "type": "object",
619 | "additionalProperties": false
620 | }
621 | }
622 | };
623 |
624 | const mappedMenuAttributes = [
625 | { name: 'drupal_internal__id', value: {} },
626 | { name: 'langcode', value: {} },
627 | { name: 'status', value: {} },
628 | { name: 'dependencies', value: {} },
629 | { name: 'third_party_settings', value: {} },
630 | { name: 'label', value: {} },
631 | { name: 'description', value: {} },
632 | { name: 'locked', value: {} },
633 | ];
634 |
635 | const mappedArticleRelationships = [
636 | {
637 | name: 'node_type',
638 | value: 'http://drupal.test/jsonapi/node/article/resource/relationships/node_type/related/schema',
639 | },
640 | {
641 | name: 'revision_uid',
642 | value: 'http://drupal.test/jsonapi/node/article/resource/relationships/revision_uid/related/schema',
643 | },
644 | {
645 | name: 'uid',
646 | value: 'http://drupal.test/jsonapi/node/article/resource/relationships/uid/related/schema',
647 | },
648 | {
649 | name: 'field_image',
650 | value: 'http://drupal.test/jsonapi/node/article/resource/relationships/field_image/related/schema',
651 | },
652 | {
653 | name: 'field_tags',
654 | value: 'http://drupal.test/jsonapi/node/article/resource/relationships/field_tags/related/schema',
655 | },
656 | ];
657 |
658 | const schemaNoDefinitions = {
659 | $schema: 'http://json-schema.org/draft-07/schema',
660 | $id: 'http://drupal.test/jsonapi/menu/menu/resource/schema.json',
661 | };
662 |
663 | const schemaNoProperties = {
664 | $schema: 'http://json-schema.org/draft-07/schema',
665 | $id: 'http://drupal.test/jsonapi/menu/menu/resource/schema.json',
666 | definitions: {},
667 | };
668 |
669 | describe('Schema Attributes', () => {
670 | test('Extract attribute names from schema definitions', () => {
671 | expect(getAttributes(schemaMenu)).toEqual(mappedMenuAttributes);
672 | expect(getAttributes(schemaArticle)).toEqual([
673 | { name: 'drupal_internal__nid', value: {} },
674 | { name: 'drupal_internal__vid', value: {} },
675 | { name: 'langcode', value: {} },
676 | { name: 'revision_timestamp', value: {} },
677 | { name: 'revision_log', value: {} },
678 | { name: 'status', value: {} },
679 | { name: 'title', value: {} },
680 | { name: 'created', value: {} },
681 | { name: 'changed', value: {} },
682 | { name: 'promote', value: {} },
683 | { name: 'sticky', value: {} },
684 | { name: 'default_langcode', value: {} },
685 | { name: 'revision_default', value: {} },
686 | { name: 'revision_translation_affected', value: {} },
687 | { name: 'path', value: {} },
688 | { name: 'body', value: {} },
689 | ]);
690 | });
691 |
692 | test('Return empty array for incomplete or empty schema', () => {
693 | expect(getAttributes(schemaNoDefinitions)).toEqual([]);
694 | expect(getAttributes(schemaNoProperties)).toEqual([]);
695 | emptyVals.forEach(val => {
696 | expect(getAttributes(val)).toEqual([]);
697 | });
698 | });
699 | });
700 |
701 | describe('Schema Includes', () => {
702 | test('Get relationship list from schema', () => {
703 | expect(getRelationships(schemaArticle)).toEqual(mappedArticleRelationships);
704 | });
705 |
706 | test('Return empty array for incomplete or empty schema', () => {
707 | expect(getRelationships(schemaNoDefinitions)).toEqual([]);
708 | expect(getRelationships(schemaNoProperties)).toEqual([]);
709 | emptyVals.forEach(val => {
710 | expect(getRelationships(val)).toEqual([]);
711 | });
712 | });
713 | });
714 |
715 | describe('Normalize Properties', () => {
716 | test('Get flattened object from nested properties', () => {
717 | expect(
718 | getRelationshipSchema(
719 | schemaArticle.definitions.relationships.properties.node_type,
720 | ),
721 | ).toEqual('http://drupal.test/jsonapi/node/article/resource/relationships/node_type/related/schema');
722 | });
723 |
724 | test('Get an empty object if recursion fails', () => {
725 | emptyVals.forEach(val => {
726 | expect(getRelationshipSchema(val)).toEqual(undefined);
727 | });
728 | });
729 |
730 | test('Map property names and values', () => {
731 | expect(
732 | mapDefinitions(schemaMenu.definitions.attributes.properties),
733 | ).toEqual(mappedMenuAttributes);
734 | });
735 |
736 | test('Map property names and processed values', () => {
737 | expect(
738 | mapDefinitions(
739 | schemaArticle.definitions.relationships.properties,
740 | getRelationshipSchema,
741 | ),
742 | ).toEqual(mappedArticleRelationships);
743 | });
744 | });
745 |
--------------------------------------------------------------------------------