@api-platform/ld
v1.0.0
Published
Fetch Edge Side APIs
Downloads
7
Keywords
Readme
@api-platform/ld
Rich JSON formats such as JSON-LD use IRIs to reference embeded data. This library fetches the wanted Linked Data automatically.
I have an API referencing books and their authors, GET /books/1
returns:
{
"@id": "/books/1",
"@type": ["https://schema.org/Book"],
"title": "Hyperion",
"author": "https://localhost/authors/1"
}
Thanks to @api-platform/ld
you can load authors automatically when you need them:
import ld from '@api-platform/ld'
const pattern = new URLPattern("/authors/:id", "https://localhost");
const books = await ld('/books', {
urlPattern: pattern,
onUpdate: (newBooks) => {
log()
}
})
function log() {
console.log(books.author?.name)
}
log()
Installation
npm install @api-platform/ld
Usage
Use ld
like fetch
and specify the URLPattern to match IRIs that are going to be fetched automatically:
import ld from '@api-platform/ld'
await ld('/books', {urlPattern: new URLPattern("/authors/:id", "https://localhost")})
Available options:
fetchFn
fetch function, defaults tofetch().then((res) => res.json())
urlPattern
the url pattern filterrelativeURIs
supports relative URIs (defaults totrue
)onUpdate: (root, options: { iri: string, data: any })
callback on data updateonError
error callback on fetch errors
URLPattern is available as a polyfil at https://www.npmjs.com/package/urlpattern-polyfill
Examples
Tanstack Query
import ld from "@api-platform/ld";
import {useEffect} from "react"
import {createRoot} from "react-dom/client"
import {
QueryClient,
useQuery,
QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient();
const pattern = new URLPattern("/(books|authors)/:id", window.origin);
function Books() {
const {isPending, error, data: books} = useQuery({
queryKey: ['/books'],
notifyOnChangeProps: 'all',
queryFn: ({queryKey}) => ld(queryKey, {
urlPattern: pattern,
onUpdate: (root, {iri, data}) => {
queryClient.setQueryData(queryKey, root)
}
})
})
if (isPending) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{books.member.map(b => (<li data-testid="book">{b?.title} - {b?.author?.name}</li>))}
</ul>
);
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<Books />
</QueryClientProvider>
)
}
createRoot(root).render(<App />)
React
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
import ld from "@api-platform/ld";
function App() {
const pattern = new URLPattern("/(books|authors)/:id", window.origin);
const [books, setBooks] = useState({});
useEffect(() => {
let ignore = false;
setBooks({});
ld('/books', {onUpdate: (books) => setBooks(books), urlPattern: pattern})
.then(books => {
if (!ignore) {
setBooks(books);
}
});
return () => {
ignore = true;
};
}, []);
return (
<ul>
{books.member?.map(b => (<li data-testid="book">{b.title} - {b.author?.name}</li>))}
</ul>
);
}
const root = createRoot(document.getElementById("root"));
root.render(<App />);
Axios
import ld from "@api-platform/ld";
const pattern = new URLPattern("/(books|authors)/:id", window.origin);
const list = document.getElementById('list')
function onUpdate(books) {
const l = []
books.member.forEach((book) => {
const li = document.createElement('li')
li.dataset.testid = 'book'
li.innerText = `${book.title} - ${book.author?.name}`
l.push(li)
});
list.replaceChildren(...l)
}
ld('/books', {urlPattern: pattern, onUpdate, fetchfn: (url, options) => axios.get(url)})
.then((books) => {
books.member.forEach((book) => {
const li = document.createElement('li')
li.dataset.testid = 'book'
li.innerText = `${book.title} - ${book.author?.name}`
list.appendChild(li)
});
})
VanillaJS
import ld from "@api-platform/ld";
const pattern = new URLPattern("/(books|authors)/:id", window.origin);
const list = document.getElementById('list')
function onUpdate(books) {
const l = []
books.member.forEach((book) => {
const li = document.createElement('li')
li.dataset.testid = 'book'
li.innerText = `${book.title} - ${book.author?.name}`
l.push(li)
});
list.replaceChildren(...l)
}
ld('/books', {urlPattern: pattern, onUpdate})
.then((books) => {
books.member.forEach((book) => {
const li = document.createElement('li')
li.dataset.testid = 'book'
li.innerText = `${book.title} - ${book.author?.name}`
list.appendChild(li)
});
})
SWR
Example of a SWR hook:
import ld, { LdOptions } from '@api-platform/ld'
import useSWR from 'swr'
import {useState} from 'react'
import type { SWRConfiguration, KeyedMutator } from 'swr'
export type fetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
export type Fetcher = (...args: any[]) => Promise<any>
export type onUpdateCallback<T> = (root: T, options: { iri: string, data: object }) => void;
export default function useSWRLd<T extends object>(url: string, fetcher: Fetcher, config: Partial<LdOptions<T>> & SWRConfiguration = {}) {
let cb: undefined | KeyedMutator<T> = undefined
// You may need to force re-rendering as the comparison algorithm of SWR does not work well when object keys are added
// another solution is to improve the compare function
const [, setRender] = useState(false)
const res = useSWR(
url,
(url: RequestInfo | URL, opts: RequestInit) =>
ld(url, {
...opts,
fetchFn: fetcher,
urlPattern: config.urlPattern,
onUpdate: (root) => {
if (cb) {
cb(root, { optimisticData: root, revalidate: false })
setRender((s: boolean) => !s)
}
},
relativeURIs: config.relativeURIs,
onError: config.onError,
}),
config
);
cb = res.mutate
return res
}