Skip to content

Commit

Permalink
wip: initialize user resources
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Mar 8, 2021
1 parent df840ae commit 31aaa10
Show file tree
Hide file tree
Showing 15 changed files with 182 additions and 33 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
7 changes: 7 additions & 0 deletions apps/conduit/bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

BASE=$1;

curl "$BASE/api/User" --upload-file resources/api/User.ttl
curl "$BASE/api/UsersCollection" --upload-file resources/api/UsersCollection.ttl
curl "$BASE/users" --upload-file resources/users.ttl
5 changes: 3 additions & 2 deletions apps/todos/package.json → apps/conduit/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "@hydrofoil/todos",
"name": "@hydrofoil/conduit",
"private": true,
"version": "0.0.0",
"scripts": {
"start": "DEBUG=knossos*,SPARQL npx ts-node ../../packages/knossos/server.ts"
"start": "DEBUG=knossos*,SPARQL npx ts-node ../../packages/knossos/server.ts",
"bootstrap": "sh bootstrap.sh http://localhost:8888"
},
"dependencies": {
"@hydrofoil/knossos": "0.0.0"
Expand Down
47 changes: 47 additions & 0 deletions apps/conduit/resources/api/User.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@prefix dash: <http://datashapes.org/dash#> .
@prefix bio: <http://purl.org/vocab/bio/0.1/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix code: <https://code.described.at/> .
@prefix schema: <http://schema.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix hydra: <http://www.w3.org/ns/hydra/core#> .

</api/User>
a hydra:Class, sh:NodeShape ;
hydra:supportedOperation
[
a schema:ReplaceAction ;
hydra:method "PUT" ;
hydra:title "Update User" ;
code:implementedBy
[
a code:EcmaScript ;
code:link <node:@hydrofoil/knossos/resource#put>
] ;
] ;
sh:property
[
sh:path foaf:email ;
sh:name "Email" ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:order 10 ;
],
[
sh:path foaf:nick ;
sh:maxCount 1 ;
sh:order 20 ;
],
[
sh:path foaf:img ;
sh:nodeKind sh:IRI ;
sh:maxCount 1 ;
sh:order 30 ;
],
[
sh:path bio:biography ;
dash:singleLine false ;
sh:maxCount 1 ;
sh:order 40 ;
] ;
.
18 changes: 18 additions & 0 deletions apps/conduit/resources/api/UsersCollection.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@prefix knossos: <https://hypermedia.app/knossos#> .
@prefix code: <https://code.described.at/> .
@prefix hydra: <http://www.w3.org/ns/hydra/core#> .

</api/UsersCollection>
a hydra:Class ;
knossos:createWithPUT true ;
hydra:supportedOperation
[
hydra:method "POST" ;
hydra:title "Register" ;
code:implementedBy
[
a code:EcmaScript ;
code:link <node:@hydrofoil/knossos/collection#post> ;
] ;
] ;
.
11 changes: 11 additions & 0 deletions apps/conduit/resources/users.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix hydra: <http://www.w3.org/ns/hydra/core#> .

</users>
a </api/UsersCollection>, hydra:Collection ;
hydra:manages
[
hydra:property rdf:type ;
hydra:object </api/User> ;
] ;
.
27 changes: 27 additions & 0 deletions packages/knossos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# @hydrofoil/knossos

Knossos is a high level Hydra server, which allows rapidly deploying Hydra-powered APIs backed by a triplestore.

### Low friction

No initial setup, you only need to create an RDF database with SPARQL Query/Update functionality.

### Turtles all the way down

The entire API is stored as RDF graph:

- The proper API resources
- Hydra API Documentation
- The data models, using SHACL Shapes

### Eating its own dog food

The API itself is also controlled using HTTP interface:

- Creating data models
- Exposing functionality as Hydra Supported Operations
- Fine-grained access control

## TODOs

- Control the implicit `PUT` on per-class basis
7 changes: 4 additions & 3 deletions packages/knossos/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Debugger } from 'debug'
import { DatasetCore, NamedNode, Term } from 'rdf-js'
import { ApiFactory } from '../../labyrinth'
import { ResourceStore } from './store'
import { createApiDocumentation, createClassesCollection } from './apiDocumentation'
import * as apiDocResources from './apiDocumentation'

interface ApiFromStore {
path?: string
Expand Down Expand Up @@ -48,8 +48,9 @@ const createApi: (arg: ApiFromStore) => ApiFactory = ({ path = '/api', store, lo
if (!apiExists) {
log('API Documentation resource does not exist. Creating...')

await store.save(createApiDocumentation(this.term))
await store.save(createClassesCollection(this.term))
await store.save(apiDocResources.ApiDocumentation(this.term))
await store.save(apiDocResources.ClassesCollection(this.term))
await store.save(apiDocResources.HydraClass())
}

const api = await store.load(this.term)
Expand Down
14 changes: 11 additions & 3 deletions packages/knossos/lib/apiDocumentation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import clownface, { GraphPointer } from 'clownface'
import { NamedNode } from 'rdf-js'
import $rdf from 'rdf-ext'
import { hydra, rdf } from '@tpluscode/rdf-ns-builders'
import { hydra, rdf, sh } from '@tpluscode/rdf-ns-builders'
import { code, query } from '@hydrofoil/labyrinth/lib/namespace'
import { fromPointer as initCollection } from '@rdfine/hydra/lib/Collection'
import {knossos} from "./namespace";

export function createApiDocumentation(term: NamedNode): GraphPointer<NamedNode> {
export function ApiDocumentation(term: NamedNode): GraphPointer<NamedNode> {
const graph = clownface({ dataset: $rdf.dataset() })

graph.node(hydra.Resource)
Expand Down Expand Up @@ -45,7 +46,7 @@ export function createApiDocumentation(term: NamedNode): GraphPointer<NamedNode>
.addOut(query.include, hydra.supportedClass)
}

export function createClassesCollection(apiDocumentation: NamedNode):GraphPointer<NamedNode> {
export function ClassesCollection(apiDocumentation: NamedNode): GraphPointer<NamedNode> {
const pointer = clownface({ dataset: $rdf.dataset() })
.namedNode(`${apiDocumentation.value}/classes`)

Expand All @@ -58,3 +59,10 @@ export function createClassesCollection(apiDocumentation: NamedNode):GraphPointe

return pointer
}

export function HydraClass(): GraphPointer<NamedNode> {
return clownface({ dataset: $rdf.dataset() })
.namedNode(hydra.Class)
.addOut(rdf.type, sh.NodeShape)
.addOut(knossos.createWithPUT, true)
}
5 changes: 5 additions & 0 deletions packages/knossos/lib/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import namespace from '@rdfjs/namespace'

type KnossosTerms = 'createWithPUT'

export const knossos = namespace<KnossosTerms>('https://hypermedia.app/knossos#')
6 changes: 4 additions & 2 deletions packages/knossos/lib/shacl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rdf } from '@tpluscode/rdf-ns-builders'
import {rdf, sh} from '@tpluscode/rdf-ns-builders'
import { shaclMiddleware } from 'hydra-box-middleware-shacl'

export const shaclValidate = shaclMiddleware({
Expand All @@ -8,7 +8,9 @@ export const shaclValidate = shaclMiddleware({
const shapes = await Promise.all(loaded)

for (const shape of shapes) {
req.shacl.shapesGraph.addAll(shape.dataset)
if (shape.has(rdf.type, sh.NodeShape).terms.length) {
req.shacl.shapesGraph.addAll(shape.dataset)
}
}

next()
Expand Down
1 change: 1 addition & 0 deletions packages/knossos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@tpluscode/rdf-ns-builders": "^0.4.0",
"@tpluscode/rdf-string": "^0.2.21",
"@tpluscode/sparql-builder": "^0.3.11",
"@rdfjs/namespace": "^1.1.0",
"@rdfine/hydra": "^0.6.4",
"middleware-async": "^1.3.1",
"clownface": "^1.2.0",
Expand Down
24 changes: 23 additions & 1 deletion packages/knossos/resource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { protectedResource } from '@hydrofoil/labyrinth/resource'
import asyncMiddleware from 'middleware-async'
import clownface, {AnyPointer, GraphPointer} from "clownface";
import {hydra, rdf} from "@tpluscode/rdf-ns-builders";
import { ResourceStore } from './lib/store'
import { shaclValidate } from './lib/shacl'
import { knossos } from './lib/namespace';

declare module 'express-serve-static-core' {
interface Request {
Expand All @@ -11,8 +14,27 @@ declare module 'express-serve-static-core' {
}
}

function assertCanBeCreateWithPut(api: AnyPointer, resource: GraphPointer) {
const types = resource.out(rdf.type)
const classes = api.has(hydra.supportedClass, types)

const anyClassAllowsPut = classes.has(knossos.createWithPUT, true).terms.length > 1
const noClassForbidsPut = classes.has(knossos.createWithPUT, false).terms.length === 0

return anyClassAllowsPut && noClassForbidsPut
}

export const put = protectedResource(shaclValidate, asyncMiddleware(async (req, res) => {
await req.knossos.store.save(await req.resource())
const api = clownface(req.hydra.api)
const resource = await req.resource()
const exists = await req.knossos.store.exists(resource.term)

if (!exists) {
assertCanBeCreateWithPut(api, resource)
resource.addOut(rdf.type, hydra.Resource)
}

await req.knossos.store.save(resource)

const updated = await req.knossos.store.load(req.hydra.resource.term)
return res.resource(updated)
Expand Down
16 changes: 13 additions & 3 deletions packages/knossos/server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import express from 'express'
import { hydraBox } from '@hydrofoil/labyrinth'
import StreamClient from 'sparql-http-client/StreamClient'
import { resource } from 'express-rdf-request'
import debug from 'debug'
import createApi from './lib/api'
import { ResourcePerGraphStore } from './lib/store'
import { resource } from 'express-rdf-request'
import { put as createResource } from './resource'

const app = express()

Expand All @@ -16,15 +17,24 @@ async function main() {
updateUrl: 'http://localhost:3030/labyrinth',
}

const store = new ResourcePerGraphStore(new StreamClient(sparql))

app.use(resource)
app.use((req, res, next) => {
req.knossos = {
store,
}
next()
})
app.use(await hydraBox({
codePath: './demo',
codePath: '.',
sparql,
loadApi: createApi({
store: new ResourcePerGraphStore(new StreamClient(sparql)),
store,
log,
}),
}))
app.put('/*', createResource)

app.listen(8888, () => log('API started'))
}
Expand Down
26 changes: 7 additions & 19 deletions packages/shacl-middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import asyncMiddleware from 'middleware-async'
import $rdf from 'rdf-ext'
import DatasetExt from 'rdf-ext/lib/Dataset'
import { NamedNode } from 'rdf-js'
import { hydra, rdf, sh } from '@tpluscode/rdf-ns-builders'
import SHACLValidator from 'rdf-validate-shacl'
import clownface, { GraphPointer } from 'clownface'
import { ProblemDocument } from 'http-problem-details'
Expand All @@ -24,10 +23,10 @@ declare module 'express-serve-static-core' {
export const shaclMiddleware = ({ loadShapes }: ShaclMiddlewareOptions): Router => {
const router = Router()

router.use(asyncMiddleware(async (req, res, next) => {
router.use(asyncMiddleware(async function initShaclGraphs(req, res, next) {
let dataGraph: GraphPointer<NamedNode>
if (!req.dataset) {
dataGraph = clownface({ dataset: $rdf.dataset() }).node(req.hydra.term)
dataGraph = clownface({dataset: $rdf.dataset()}).node(req.hydra.term)
} else {
dataGraph = await req.resource()
}
Expand All @@ -41,7 +40,8 @@ export const shaclMiddleware = ({ loadShapes }: ShaclMiddlewareOptions): Router

router.use(asyncMiddleware(loadShapes))

router.use(asyncMiddleware(async (req, res, next) => {
// TODO: Load data from linked instances to be able to validate their type
router.use(asyncMiddleware(async function validateShapes(req, res, next) {
if (req.shacl.shapesGraph.size === 0) {
return next()
}
Expand Down Expand Up @@ -70,22 +70,9 @@ export const shaclMiddleware = ({ loadShapes }: ShaclMiddlewareOptions): Router
return router
}

export const shaclMiddleware = ({ loadResource, loadResourcesTypes }: ShaclMiddlewareOptions) => asyncMiddleware(async (req, res, next) => {
/*
export const shaclMiddleware = ({ loadResourcesTypes }: ShaclMiddlewareOptions) => asyncMiddleware(async (req, res, next) => {
const shapes = $rdf.dataset()
await Promise.all(req.hydra.operation.out(hydra.expects).map(async (expects) => {
if (expects.term.termType !== 'NamedNode') return

const pointer = await loadResource(expects.term)
if (pointer?.has(rdf.type, [sh.NodeShape]).values.length) {
await shapes.addAll([...pointer.dataset])

if (pointer.out([sh.targetClass, sh.targetNode, sh.targetObjectsOf, sh.targetSubjectsOf]).values.length === 0) {
shapes.add($rdf.quad(pointer.term, sh.targetNode, resource.term))
}

resource.addOut(rdf.type, pointer.out(sh.targetClass))
}
}))
// Load data from linked instances to be able to validate their type
const classProperties = clownface({ dataset: shapes })
Expand All @@ -97,3 +84,4 @@ export const shaclMiddleware = ({ loadResource, loadResourcesTypes }: ShaclMiddl
const dataset = $rdf.dataset([...resource.dataset, ...linkedInstancesQuads])
})
*/

0 comments on commit 31aaa10

Please sign in to comment.