Skip to content

Commit

Permalink
Remove dependency on @zazuko/env (#130)
Browse files Browse the repository at this point in the history
* build(deps): remove dependency on ``@zazuko/env`

* refactor: no env dependency

* fix: no namespace builder usage
  • Loading branch information
tpluscode authored Mar 30, 2024
1 parent 1cf0916 commit ed24e8d
Show file tree
Hide file tree
Showing 23 changed files with 318 additions and 188 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-mirrors-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kopflos-cms/core": patch
---

`Api` exported also as interface
5 changes: 5 additions & 0 deletions .changeset/new-poems-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kopflos-cms/core": minor
---

RDF/JS Environment must be passed explicitly when initialising an API
51 changes: 33 additions & 18 deletions Api.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
/* eslint-disable camelcase */
import rdf from '@zazuko/env-node'
import EcmaScriptLoader from 'rdf-loader-code/ecmaScript.js'
import LoaderRegistryImpl, { LoaderRegistry } from 'rdf-loaders-registry'
import EcmaScriptModuleLoader from 'rdf-loader-code/ecmaScriptModule.js'
import EcmaScriptLiteralLoader from 'rdf-loader-code/ecmaScriptLiteral.js'
import type { NamedNode, DatasetCore, Quad_Graph } from '@rdfjs/types'
import type { NamedNode, Quad_Graph, DatasetCore } from '@rdfjs/types'
import addAll from 'rdf-dataset-ext/addAll.js'
import fromStream from 'rdf-dataset-ext/fromStream.js'
import { replaceDatasetIRI } from './lib/replaceIRI.js'
import Factory from './lib/factory.js'

interface ApiInit<D extends DatasetCore = DatasetCore> {
term?: NamedNode
dataset?: D
graph?: NamedNode
path?: string
codePath?: string
factory: Factory<D>
}

class Api {
export interface Api<D extends DatasetCore = DatasetCore> {
env: Factory<D>
initialized: boolean
path: string
codePath: string
graph?: Quad_Graph | undefined
dataset: DatasetCore
dataset: D
term: NamedNode | undefined
loaderRegistry: LoaderRegistry
init(): Promise<void>
}

export default class Impl<D extends DatasetCore = DatasetCore> implements Api<D> {
initialized: boolean
path: string
codePath: string
graph?: Quad_Graph | undefined
dataset: D
private _term: NamedNode | undefined
loaderRegistry: LoaderRegistry
private _initialization?: Promise<void>
readonly tasks: Array<() => Promise<void>>
readonly env: Factory<D>

constructor({ term, dataset, graph, path = '/api', codePath = process.cwd() }: ApiInit = { }) {
constructor({ term, dataset, graph, path = '/api', codePath = process.cwd(), factory }: ApiInit<D>) {
this._term = term
this.dataset = dataset || rdf.dataset()
this.env = factory
this.dataset = dataset || factory.dataset()
this.graph = graph
this.path = path
this.codePath = codePath
Expand Down Expand Up @@ -59,45 +76,43 @@ class Api {

fromFile(filePath: string) {
this.tasks.push(async () => {
rdf.dataset().addAll.call(this.dataset, await rdf.dataset().import(rdf.fromFile(filePath)))
addAll(this.dataset, await fromStream(this.env.dataset(), this.env.fromFile(filePath)))
})

return this
}

rebase(fromBaseIRI: string | NamedNode, toBaseIRI: string | NamedNode) {
this.tasks.push(async () => {
this.dataset = replaceDatasetIRI(fromBaseIRI, toBaseIRI, this.dataset)
this.dataset = replaceDatasetIRI(fromBaseIRI, toBaseIRI, this.dataset, this.env)
})

return this
}

static fromFile(filePath: string, options?: ApiInit) {
const api = new Api(options)
static fromFile(filePath: string, options: ApiInit) {
const api = new Impl(options)

return api.fromFile(filePath)
}

async _beginInit() {
if (!this.dataset) {
this.dataset = rdf.dataset()
this.dataset = this.env.dataset()
}

for (const task of this.tasks) {
await task()
}

const apiDoc = rdf.clownface({ dataset: this.dataset, term: this.term, graph: this.graph })
const apiDoc = this.env.clownface({ dataset: this.dataset, term: this.term, graph: this.graph })

if (apiDoc.has(rdf.ns.rdf.type, rdf.ns.hydra.ApiDocumentation).terms.length === 0) {
apiDoc.addOut(rdf.ns.rdf.type, rdf.ns.hydra.ApiDocumentation)
if (apiDoc.has(this.env.ns.rdf.type, this.env.ns.hydra.ApiDocumentation).terms.length === 0) {
apiDoc.addOut(this.env.ns.rdf.type, this.env.ns.hydra.ApiDocumentation)

apiDoc.any().has(rdf.ns.rdf.type, rdf.ns.hydra.Class).forEach(supportedClass => {
apiDoc.addOut(rdf.ns.hydra.supportedClass, supportedClass)
apiDoc.any().has(this.env.ns.rdf.type, this.env.ns.hydra.Class).forEach(supportedClass => {
apiDoc.addOut(this.env.ns.hydra.supportedClass, supportedClass)
})
}
}
}

export default Api
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,28 @@ const store = new FlatMultiFileStore({
```

An `Api` object contains the dataset of the API documentation, where to find it and where to find the code.
The static method `.fromFile` reads the dataset from the given file and creates an `Api` object with the given options.
The static method `.fromFile` reads the dataset from the given file and creates an `Api` object with the given options.

```javascript
import rdf from '@zazuko/env-node'

const api = await Api.fromFile('api.ttl', {
factory: rdf,
path: '/api',
codePath: __dirname
})
```

The `factory` parameter is required, and must be an [RDF/JS Environment](https://npm.im/@rdfjs/environment), providing the following factories compatible with the following:

- [DatasetCoreFactory](https://npm.im/@rdfjs/dataset)
- [DataFactory](https://npm.im/@rdfjs/data-model)
- [NamespaceFactory](https://npm.im/@rdfjs/namespace)
- [TermSetFactory](https://npm.im/@rdfjs/term-set)
- [ClownfaceFactory](https://npm.im/clownface)
- [NsBuildersFactory](https://npm.im/@tpluscode/rdf-ns-builders)
- [FsUtilsFactory](https://npm.im/@rdfjs/@zazuko/rdf-utils-fs)

Once both objects are created, the middleware can be used:

```javascript
Expand All @@ -45,7 +58,7 @@ app.listen(9000)
The operations must implement a [Express routing handler](http://expressjs.com/en/starter/basic-routing.html) interface (`(req, res, next) => {}`).
@kopflos-cms/core adds the [@rdfjs/express-handler](https://github.com/rdfjs-base/express-handler) to handle incoming and outgoing RDF data.
For `GET` requests with a matching IRI Template, the `.dataset()` and `.quadStream()` as defined by `express-handler` are also available to read the given variables.
Additionally there is a `hydra` property assigned to `req` that contains more data about the request:
Additionally, there is a `hydra` property assigned to `req` that contains more data about the request:

```javascript
req.hydra = {
Expand Down
30 changes: 17 additions & 13 deletions StoreResourceLoader.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import type { NamedNode, Store } from '@rdfjs/types'
import rdf from '@zazuko/env-node'
import type { DatasetCore, NamedNode, Store } from '@rdfjs/types'
import { isNamedNode } from 'is-graph-pointer'
import fromStream from 'rdf-dataset-ext/fromStream.js'
import toStream from 'rdf-dataset-ext/toStream.js'
import Factory from './lib/factory.js'
import { PropertyResource, Resource, ResourceLoader } from './index.js'

export default class StoreResourceLoader implements ResourceLoader {
export default class StoreResourceLoader<D extends DatasetCore> implements ResourceLoader<D> {
readonly store: Store
private env: Factory<D>

constructor({ store }: { store: Store }) {
constructor({ store, env }: { store: Store; env: Factory<D> }) {
this.store = store
this.env = env
}

async load(term: NamedNode): Promise<Resource | null> {
const dataset = await rdf.dataset().import(this.store.match(null, null, null, term))
async load(term: NamedNode): Promise<Resource<D> | null> {
const dataset = await fromStream(this.env.dataset(), this.store.match(null, null, null, term))

if (dataset.size === 0) {
return null
}

const types = rdf.clownface({ dataset, term })
.out(rdf.ns.rdf.type)
const types = this.env.clownface({ dataset, term })
.out(this.env.ns.rdf.type)
.filter(isNamedNode)

return {
Expand All @@ -28,9 +32,9 @@ export default class StoreResourceLoader implements ResourceLoader {
return dataset
},
quadStream() {
return dataset.toStream()
return toStream(dataset)
},
types: rdf.termSet(types.terms),
types: this.env.termSet(types.terms),
}
}

Expand All @@ -40,9 +44,9 @@ export default class StoreResourceLoader implements ResourceLoader {
return resource ? [resource] : []
}

async forPropertyOperation(term: NamedNode): Promise<PropertyResource[]> {
const dataset = await rdf.dataset().import(this.store.match(null, null, term, null))
const result: PropertyResource[] = []
async forPropertyOperation(term: NamedNode): Promise<PropertyResource<D>[]> {
const dataset = await fromStream(this.env.dataset(), this.store.match(null, null, term, null))
const result: PropertyResource<D>[] = []

for (const quad of dataset) {
if (quad.subject.termType !== 'NamedNode') continue
Expand Down
2 changes: 2 additions & 0 deletions examples/blog/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from 'express'
import FlatMultiFileStore from 'rdf-store-fs/FlatMultiFileStore.js'
import rdf from '@zazuko/env-node'
import hydraBox from '../../middleware.js'
import Api from '../../Api.js'
import ResourceStore from './lib/ResourceStore.js'
Expand All @@ -13,6 +14,7 @@ async function main() {
})

const api = await Api.fromFile('api.ttl', {
factory: rdf,
path: '/api',
codePath: __dirname,
})
Expand Down
36 changes: 16 additions & 20 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,39 @@ import type { Readable } from 'stream'
import type * as RDF from '@rdfjs/types'
import type { GraphPointer } from 'clownface'
import type { Request } from 'express'
import type { Dataset } from '@zazuko/env/lib/Dataset.js'
import middleware from './middleware.js'
import Api from './Api.js'
import type { Api } from './Api.js'

export interface Resource {
export { default as middleware } from './middleware.js'
export { default as Api } from './Api.js'

export interface Resource<D extends RDF.DatasetCore = RDF.DatasetCore> {
term: RDF.NamedNode
prefetchDataset: RDF.DatasetCore
dataset(): Promise<Dataset>
dataset(): Promise<D>
quadStream(): RDF.Stream & Readable
types: Set<RDF.NamedNode>
}

export interface PropertyResource extends Resource {
export interface PropertyResource<D extends RDF.DatasetCore = RDF.DatasetCore> extends Resource<D> {
property: RDF.Quad_Predicate
object: RDF.NamedNode
}

export interface PotentialOperation {
resource: Resource | PropertyResource
export interface PotentialOperation<D extends RDF.DatasetCore = RDF.DatasetCore> {
resource: Resource<D> | PropertyResource<D>
operation: GraphPointer
}

export interface HydraBox {
api: Api
export interface HydraBox<D extends RDF.DatasetCore = RDF.DatasetCore> {
api: Api<D>
term: RDF.NamedNode
store: RDF.Store
resource: Resource & { clownface(): Promise<GraphPointer<RDF.NamedNode, Dataset>> }
resource: Resource<D> & { clownface(): Promise<GraphPointer<RDF.NamedNode, D>> }
operation: GraphPointer
operations: PotentialOperation[]
}

export interface ResourceLoader {
forClassOperation(term: RDF.NamedNode, req: Request): Promise<Resource[]>
forPropertyOperation(term: RDF.NamedNode, req: Request): Promise<PropertyResource[]>
operations: PotentialOperation<D>[]
}

export default {
middleware,
Api,
export interface ResourceLoader<D extends RDF.DatasetCore = RDF.DatasetCore> {
forClassOperation(term: RDF.NamedNode, req: Request): Promise<Resource<D>[]>
forPropertyOperation(term: RDF.NamedNode, req: Request): Promise<PropertyResource<D>[]>
}
11 changes: 11 additions & 0 deletions lib/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Environment } from '@rdfjs/environment/Environment.js'
import type { DataFactory, DatasetCoreFactory, Quad, DatasetCore } from '@rdfjs/types'
import type { TermSetFactory } from '@rdfjs/term-set/Factory.js'
import type ClownfaceFactory from 'clownface/Factory.js'
import type NsBuildersFactory from '@tpluscode/rdf-ns-builders'
import type FsUtilsFactory from '@zazuko/rdf-utils-fs/Factory.js'
import type { NamespaceFactory } from '@rdfjs/namespace/Factory.js'

type Factory<D extends DatasetCore = DatasetCore> = Environment<DatasetCoreFactory<Quad, Quad, D> | DataFactory | TermSetFactory | ClownfaceFactory | NsBuildersFactory | FsUtilsFactory | NamespaceFactory>

export default Factory
2 changes: 1 addition & 1 deletion lib/middleware/apiHeader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router } from 'express'
import Api from '../../Api.js'
import { Api } from '../../Api.js'

export default function factory(api: Api) {
const router = Router()
Expand Down
17 changes: 9 additions & 8 deletions lib/middleware/iriTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import uriTemplateRoute from 'uri-template-route'
import rdf from '@zazuko/env-node'
import { RequestHandler, Router } from 'express'
import type { GraphPointer } from 'clownface'
import type { NamedNode } from '@rdfjs/types'
import toStream from 'rdf-dataset-ext/toStream.js'
import log from '../log.js'
import Api from '../../Api.js'
import { Api } from '../../Api.js'
import Factory from '../factory.js'

const { debug } = log('iriTemplate')

const literalValueRegex = /^"(?<value>.+)"(@|\^\^)?((?<=@)(?<language>.*))?((?<=\^\^)(?<datatype>.*))?$/

function createTermFromVariable({ template, value }: { template: GraphPointer; value: string }) {
function createTermFromVariable({ template, value, rdf }: { template: GraphPointer; value: string; rdf: Factory }) {
if (!rdf.ns.hydra.ExplicitRepresentation.equals(template.out(rdf.ns.hydra.variableRepresentation).term)) {
return value
}
Expand All @@ -33,7 +34,7 @@ function createTermFromVariable({ template, value }: { template: GraphPointer; v
return values.map(parseValue)
}

function middleware(pointer: GraphPointer): RequestHandler {
function middleware(pointer: GraphPointer, rdf: Factory): RequestHandler {
const iriTemplateNode = rdf.clownface(pointer)
const template = iriTemplateNode.out(rdf.ns.hydra.template).value

Expand Down Expand Up @@ -67,28 +68,28 @@ function middleware(pointer: GraphPointer): RequestHandler {
return
}

templateParams.addOut(property, createTermFromVariable({ template: iriTemplateNode, value }))
templateParams.addOut(property, createTermFromVariable({ template: iriTemplateNode, value, rdf }))
})

req.dataset = () => {
return Promise.resolve(templateParams.dataset)
}

req.quadStream = () => {
return templateParams.dataset.toStream()
return toStream(templateParams.dataset)
}

next()
})
}

export default function factory({ dataset, graph }: Pick<Api, 'dataset' | 'graph'>) {
export default function factory({ dataset, graph, env: rdf }: Pick<Api, 'dataset' | 'graph' | 'env'>) {
const node = rdf.clownface({ dataset, graph })
const router = Router()

node.has(rdf.ns.rdf.type, rdf.ns.hydra.IriTemplate).forEach(iriTemplateNode => {
debug('Creating route for IriTemplate', iriTemplateNode.term.value)
router.use(middleware(iriTemplateNode))
router.use(middleware(iriTemplateNode, rdf))
})

return router
Expand Down
Loading

0 comments on commit ed24e8d

Please sign in to comment.