Skip to content

Commit

Permalink
Merge pull request #36 from zazuko/kopflos
Browse files Browse the repository at this point in the history
Kopflos
  • Loading branch information
tpluscode authored Nov 6, 2024
2 parents bde1713 + 21edc0d commit 5c3833a
Show file tree
Hide file tree
Showing 29 changed files with 1,498 additions and 5 deletions.
19 changes: 19 additions & 0 deletions docs/apis/apis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
sidebar_position: 1
---

# Data-Centric APIs

Data-centric APIs are designed to provide a structured and standardized way to access and
manipulate data. Unlike traditional APIs that focus on specific functionalities or services,
data-centric APIs emphasize the data itself, making it easier to integrate, query, and manage data
across different systems. These APIs often leverage semantic web technologies, such as RDF and SPARQL,
to enable rich data interactions and ensure interoperability between diverse data sources.

## Kopflos

[Kopflos](./kopflos) is Zazuko's data-centric API platform that provides a flexible solution for managing and
exposing data as APIs. The name, "Kopflos," is a German word that means "headless," reflecting the
platform's focus on data rather than presentation. However, Kopflos is flexible enough to accommodate
various data models and use cases, making it suitable for a wide range of applications, full-featured
websites using the latest Web technologies.
2 changes: 2 additions & 0 deletions docs/apis/kopflos/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: Kopflos
position: 2
2 changes: 2 additions & 0 deletions docs/apis/kopflos/explanations/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: Explanations
position: 3
70 changes: 70 additions & 0 deletions docs/apis/kopflos/explanations/request-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Request pipeline
sidebar_position: 1
---

# Kopflos request pipeline

```
Incoming Request
└─▶ Kopflos handler
4**/5** ◀─┴─▶ Resource Shape Lookup
4**/5** ◀─┴─▶ Resource Loader Lookup
4**/5** ◀─┴─▶ Load Resource
4** ◀─┴─▶ Authorization
400 ◀─┴─▶ Validation
4**/5** ◀─┴─▶ (User handler)
└─▶ Reply
```

## Incoming request

Incoming request is handled by the server library, such as express or fastify and then forwarded to Kopflos.

## Kopflos handler

Kopflos handler is the main entry point for all incoming requests. It is responsible for orchestrating the request pipeline.

## Resource Shape Lookup

Resource Shape Lookup executes SPARQL against the `default` query endpoint to find the shape targeting the requested resource.

:::tip
See also: [How to Select which resources should be served by the API](../how-to/resource-shape.md)
:::

## Resource Loader Lookup + Load Resource

When the Resource Shape is found, a resource loader is selected based from `kopflos:resourceLoader` property, going bottom-up from the Resource/Property Shape to the share `kopflos:Config` resource.

It is used to load the requested resource's Core Representation.

:::info
The Core Representation are the triples returned by the resource loader. Typically, that would be the result of a SPARQL `DESCRIBE` query or contents of resource's "own graph".
:::

:::warning
By default, a loader which returns the resource's own graph is used.
:::

## Authorization

Not implemented yet.

## Validation

Not implemented yet.

## User handler

Finally, the handler is executed. If no handler is defined, and the request method is a GET, the resource's Core Representation is returned.

The result of the handler is forwarded back to the server library to be sent as a response.
2 changes: 2 additions & 0 deletions docs/apis/kopflos/how-to/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: How-To
position: 2
87 changes: 87 additions & 0 deletions docs/apis/kopflos/how-to/express-middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Use express middleware

The package `@kopflos-cms/express` allows you to use existing express middleware in your Kopflos project.
To do that, add a plugin `@kopflos-cms/express/middleware` to your `kopflos.config.js` file with `before`
and `after` keys.

The `after` middleware are only executed when kopflos handler returns a `404` status code or throws an error.

## Middleware from NPM packages

In your `kopflos.config.js` file, you can add references to other modules, such as `cors` or `compression`.

The requirements are that every such module default-exports a function which returns the actual middleware.

To provide an additional config, add a two element array instead of just the module name, as shown below
on the example of `compression`.

```javascript
export default {
// ...other settings
plugins: {
'@kopflos-cms/express/middleware': {
before: [
'cors',
['compression', { level: 9 }],
],
},
},
}
```

You can also reference named exports by adding a `#name` after the modue name. For example,
to use [express-rate-limit](https://www.npmjs.com/package/express-rate-limit), you can do the following:

```javascript
export default {
// ...other settings
plugins: {
'@kopflos-cms/express/middleware': {
before: [
['express-rate-limit#rateLimit', {
windowMs: 15 * 60 * 1000,
limit: 100,
}],
],
},
},
}
```

## Your own middleware or NPM packages which do not export a function

If you want to include your own middleware or a package which does not export a function, you can do so by
exporting a function from your own module.

```js
// ./lib/my-middleware.js
export default function myMiddleware() {
return (req, res, next) => {
// your middleware code here
next();
};
}
```

Then, you can reference it in your `kopflos.config.js` file by the absolute path:

```javascript
import * as url from 'node:url'

export default {
// ...other settings
plugins: {
'@kopflos-cms/express/middleware': {
before: [
url.fileURLToPath(new URL('./lib/my-middleware.js', import.meta.url)),
],
},
},
}
```
:::warning
The path must be absolute, thus the usage of `import.meta.url` to resolve the path relative to the
config file itself. For that reason, this method will only work with configuration files written as
code. (for commonjs, you can use `__dirname` instead)
:::
170 changes: 170 additions & 0 deletions docs/apis/kopflos/how-to/html-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Serve RDF in data-bound HTML

This guide will show you how to serve HTML templates bound to RDF data.

:::warning
These features are a work in progress.
:::

## Prerequisites

In an existing kopflos application, install the following dependencies:

```sh
npm install @kopflos-labs/html-template @kopflos-labs/handlebars
```

## Create a template

A template is a combination of HTML `<template>` elements and Handlebars expressions. It is
important,
however, that the HTML is only processed on the server and not in the browser. This is where the
handlebars package comes in, providing a familiar syntax to fill in the templates with your data.

First, create a new HTML file, for example `/templates/page.html` which will be used to serve a page
with a title and a body simple. We will be serving instances of the `schema:Person` class.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<template target-class="schema:Person">
<template property="schema:name">
<title>{{ pointer.value }}</title>
</template>
</template>
</head>
<body>
<template target-class="schema:Person">
<h1>{{ valueof schema:name }}</h1>

<template property="schema:address">
<dl>
<dt>Street</dt>
<dd>{{ valueof schema:streetAddress }}</dd>
<dt>City</dt>
<dd>{{ valueof schema:addressLocality }}</dd>
<dt>Postal Code</dt>
<dd>{{ valueof schema:postalCode }}</dd>
</dl>
</template>
</template>
</body>
</html>
```

The template elements are used to find the correct data to fill in the placeholders.

Similarly to [SHACL's `sh:targetClass`](https://www.w3.org/TR/shacl/#targetClass), the
`target-class`
attribute is used to find the root node in the template data. It is required on the top level
template
element.

Templates with a `property` attribute are used to navigate the data graph, just like
[Property Shapes](https://www.w3.org/TR/shacl/#property-shapes).

Inside any template, you can use template binding expressions to fill in the data.

:::tip
Here we use handlebars, but it is possible to replace it with other templating languages.
:::

The current node in the data graph is available as `pointer`. The `valueof` helper is used to output
value of a property, thus being an alternative to nested templates with `property` attributes.
For example, since complex properties are supported,

```html
<img src="{{ valueof schema:image/schema:tumbnail/schema:contentUrl }}">
```

is a terser equivalent to:

```html

<template property="schema:image">
<template property="schema:thumbnail">
<template property="schema:contentUrl">
<img src="{{ pointer.value }}">
</template>
</template>
</template>
```

## Create a page resource shape

To serve the template, we need to create [Resource Shape](./resource-shape.md) which will
act as a dynamic resource, meaning that it will serve resources which do no actually exist in the
store. Instead, other resource data will be fetched and the page itself is only a URL pattern. In
other words, a single resource shape will match and serve multiple resources.

```turtle
PREFIX sh: <http://www.w3.org/ns/shacl#>
PREFIX kl: <https://kopflos.described.at/>
PREFIX code: <https://code.described.at/>
<person-page>
a kl:ResourceShape ;
kl:api <> ;
sh:target
[
a kl:PatternedTarget ;
kl:regex "/page/person/(?<id>\\w+)$" ;
] ;
kl:handler
[
a kl:Handler ;
kl:method "GET" ;
code:implementedBy
(
[
a code:EcmaScriptModule ;
code:link <node:@kopflos-cms/serve-file#default> ;
code:arguments ( "templates/person.html" ) ;
]
[
a code:EcmaScriptModule ;
code:link <node:@kopflos-labs/html-template#default> ;
code:arguments
(
[
a code:EcmaScriptModule ;
code:link <node:@kopflos-labs/handlebars#default> ;
]
[
a code:EcmaScriptModule ;
code:link <file:lib/templateData.js#describe> ;
]
"/person/${id}"^^code:EcmaScriptTemplateLiteral
) ;
]
)
] ;
.
```

The patterned target is used to match the URL pattern similar to `/page/person/:id` where `:id` is
stored as a [request subject variable](../reference/request-handlers#subject-variables).

The handler is a sequence of modules which will load the template file using `@kopflos-cms/serve-file`
and the process the templates using `@kopflos-labs/html-template` and `@kopflos-labs/handlebars`.

## Create a data source

Finally, you need to create the `lib/templateData.js#describe` module which will provide the data
for the template:

```ts
import type { TemplateDataFunc } from '@kopflos-labs/html-template'

export const describe = (resourcePath: string): TemplateDataFunc => ({ env }) => {
return env.sparql.default.stream.query.construct(`
BASE <${env.kopflos.config.baseIri}>
DESCRIBE <${resourcePath}>`)
}
```

The implementation is a minimal query to describe the person resource, whose identifier is taken from
the resource pattern. Notice how it's declaratively set in the handler as a template literal and
forwarded to the `describe` function.
34 changes: 34 additions & 0 deletions docs/apis/kopflos/how-to/load-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Load API description

A Kopflos API is composed of triples that describe the API's structure and behavior. These triples need to be loaded into the API's store before the API can be used.

## Load from named graphs

The simplest and recommended way to load the API is to use named graphs. A single API can be loaded from multiple named graphs, and there can be multiple APIs served from a single Kopflos instance. Use the code below to load the API from named graphs:

```js
import Kopflos from '@kopflos-cms/core'

let config
const api = new Kopflos(config)
await Kopflos.fromGraphs(api, 'http://example.com/api1', 'http://example.com/api2', 'http://example.com/shared')
```

:::tip
[RDF/JS NamedNode](https://rdf.js.org/data-model-spec/#namednode-interface) objects can be used as well.
:::

The given named graphs will be loaded from the default SPARQL endpoint.

## Initialise with preloaded data

If you have the API triples in memory, you can initialise the API with it directly:

```typescript
import Kopflos from '@kopflos-cms/core'

let config
let dataset: DatasetCore

const api = new Kopflos(config, { dataset })
```
Loading

0 comments on commit 5c3833a

Please sign in to comment.