@elastic-suite/gally-admin-shared
Version:
Shared package for gally admin BO
193 lines (179 loc) • 5.94 kB
text/typescript
import jsonld, { JsonLdDocument } from 'jsonld'
import { headerRegexp } from '../constants'
import {
IApi,
IEntrypoint,
IExpandedDocsJsonld,
IExpandedEntrypoint,
IExpandedHydraProperty,
IExpandedHydraSupportedClass,
IJsonldId,
IJsonldRange,
IResource,
} from '../types'
import { fetchJson } from './fetch'
type IExpandedHydraSupportedClassMap = Map<string, IExpandedHydraSupportedClass>
export function getDocumentationUrlFromHeaders(
headers: Headers,
apiUrl: string
): string {
if (process.env.NEXT_PUBLIC_LOCAL) {
return `${apiUrl}/docs.jsonld`
}
const linkHeader = headers.get('Link')
if (linkHeader) {
const matches = headerRegexp.exec(linkHeader)
return matches[1]
}
}
export async function fetchDocs(apiUrl: string): Promise<{
docs: IExpandedDocsJsonld
docsUrl: string
entrypoint: IExpandedEntrypoint
entrypointUrl: string
}> {
const { json: entrypoint, response } = await fetchJson<IEntrypoint>(apiUrl, {
headers: { 'Content-Type': 'application/ld+json' },
})
const docsUrl = getDocumentationUrlFromHeaders(response.headers, apiUrl)
const { json: docs } = await fetchJson<JsonLdDocument>(docsUrl, {
headers: { 'Content-Type': 'application/ld+json' },
})
const [expandedDoc, expandedEntrypoint] = await Promise.all([
jsonld.expand(docs, { base: docsUrl }),
jsonld.expand(entrypoint, { base: apiUrl }),
])
return {
docs: expandedDoc[0] as unknown as IExpandedDocsJsonld,
docsUrl,
entrypoint: expandedEntrypoint[0] as unknown as IExpandedEntrypoint,
entrypointUrl: apiUrl,
}
}
export function getSupportedClassMap(
jsonld: IExpandedDocsJsonld
): IExpandedHydraSupportedClassMap {
return new Map(
jsonld['http://www.w3.org/ns/hydra/core#supportedClass'].map(
(supportedClass) => [supportedClass['@id'], supportedClass]
)
)
}
export function isIJsonldRange(
range: IJsonldId | IJsonldRange
): range is IJsonldRange {
return 'http://www.w3.org/2002/07/owl#equivalentClass' in range
}
export function findRelatedClass(
supportedClassMap: IExpandedHydraSupportedClassMap,
property: IExpandedHydraProperty
): IExpandedHydraSupportedClass {
if (property['http://www.w3.org/2000/01/rdf-schema#range'] instanceof Array) {
for (const range of property[
'http://www.w3.org/2000/01/rdf-schema#range'
]) {
if (isIJsonldRange(range)) {
const onProperty =
range['http://www.w3.org/2002/07/owl#equivalentClass'][0][
'http://www.w3.org/2002/07/owl#onProperty'
][0]['@id']
const allValuesFrom =
range['http://www.w3.org/2002/07/owl#equivalentClass'][0][
'http://www.w3.org/2002/07/owl#allValuesFrom'
][0]['@id']
if (
allValuesFrom &&
onProperty === 'http://www.w3.org/ns/hydra/core#member'
) {
return supportedClassMap.get(allValuesFrom)
}
}
}
}
// As a fallback, find an operation available on the property of the entrypoint returning the searched type (usually POST)
for (const entrypointSupportedOperation of property[
'http://www.w3.org/ns/hydra/core#supportedOperation'
] || []) {
if (
!entrypointSupportedOperation['http://www.w3.org/ns/hydra/core#returns']
) {
continue
}
const returns =
entrypointSupportedOperation[
'http://www.w3.org/ns/hydra/core#returns'
][0]['@id']
if (
'string' === typeof returns &&
returns.indexOf('http://www.w3.org/ns/hydra/core') !== 0
) {
return supportedClassMap.get(returns)
}
}
}
export function simplifyJsonldObject(
property: Record<string, unknown>
): unknown {
if ('@value' in property) {
return property['@value']
}
return Object.fromEntries(
Object.entries(property).map(([key, value]) => {
const name = key.substring(key.lastIndexOf('#') + 1)
if (!(value instanceof Array)) {
if (!(value instanceof Object)) {
return [name, value]
}
return [name, simplifyJsonldObject(value as Record<string, unknown>)]
}
if (value.length === 1) {
if (!(value[0] instanceof Object)) {
return [name, value[0]]
}
return [name, simplifyJsonldObject(value[0])]
}
if (!(value[0] instanceof Object)) {
return [name, value]
}
return [name, value.map(simplifyJsonldObject)]
})
)
}
export async function parseSchema(apiUrl: string): Promise<IApi> {
const { docs, entrypoint } = await fetchDocs(apiUrl)
const supportedClassMap = getSupportedClassMap(docs)
const entrypointClass = supportedClassMap.get(entrypoint['@type'][0])
let resources = []
if (entrypointClass) {
resources = entrypointClass[
'http://www.w3.org/ns/hydra/core#supportedProperty'
].reduce((acc, supportedProperty) => {
if (supportedProperty['http://www.w3.org/ns/hydra/core#property'][0]) {
const [property] =
supportedProperty['http://www.w3.org/ns/hydra/core#property']
const relatedClass = findRelatedClass(supportedClassMap, property)
if (relatedClass && entrypoint[property['@id']]) {
const resource = simplifyJsonldObject({
...relatedClass,
'http://www.w3.org/ns/hydra/core#supportedOperation': [
...property['http://www.w3.org/ns/hydra/core#supportedOperation'],
...relatedClass[
'http://www.w3.org/ns/hydra/core#supportedOperation'
],
],
} as unknown as Record<string, unknown>) as Omit<IResource, 'url'>
const [entrypointUrl] = entrypoint[property['@id']]
acc.push({
...resource,
url:
typeof entrypointUrl === 'string'
? entrypointUrl
: entrypointUrl['@id'],
})
}
}
return acc
}, [])
}
return resources
}