Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(kopflos): CLI #31

Merged
merged 6 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
2 changes: 1 addition & 1 deletion docs/apis/kopflos/how-to/load-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const api = new Kopflos(config)
await Kopflos.fromGraphs(api, 'http://example.com/api1', 'http://example.com/api2', 'http://example.com/shared')
```

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

Expand Down
80 changes: 80 additions & 0 deletions docs/apis/kopflos/how-to/parametrised-handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Implement parametrised handlers

## Positional arguments

It is possible to parametrise a handler so that it can be more easily adapted to various use cases.

For example, a handler which runs queries could be configured to run against different
endpoints configured in the app. To use such a handler, we can pass the endpoint as a parameter.
That is done by adding a list or object of `code:arguments` property inside the implementation node.

```turtle
PREFIX code: <https://code.described.at/>
PREFIX kl: <https://kopflos.described.at/>

[
a kl:Handler ;
code:implementedBy [
a code:EcamScriptModule ;
code:link <file:handler/query.js#runQuery> ;
code:arguments ( "wikidata" ) ;
] ;
] .
```

The handler implementation can then access the arguments as a parameters passed to the handler factory.

```ts
// handler/query.js#runQuery
import type { Handler } from '@kopflos-cms/core'

export function runQuery (endpointName: string): Handler {
return ({ env }) => {
const endpoint = env.sparql[endpointName]
if (!endpoint) {
throw new Error(`Unknown endpoint: ${endpointName}`)
}

// Run the query against the endpoint
}
}
```

## Named arguments

Named arguments are also supported.

```turtle
PREFIX code: <https://code.described.at/>
PREFIX arg: <https://code.described.at/argument#>
PREFIX kl: <https://kopflos.described.at/>

[
a kl:Handler ;
code:implementedBy [
a code:EcamScriptModule ;
code:link <file:handler/query.js#runQuery> ;
code:arguments [
arg:endpointName "wikidata" ;
] ;
] ;
] .
```

The handler implementation can then access the arguments as a parameters passed to the handler factory.

```ts
// handler/query.js#runQuery
import type { Handler } from '@kopflos-cms/core'

export function runQuery ({ endpointName }): Handler {
return ({ env }) => {
const endpoint = env.sparql[endpointName]
if (!endpoint) {
throw new Error(`Unknown endpoint: ${endpointName}`)
}

// Run the query against the endpoint
}
}
```
Loading
Loading