Skip to content

Commit

Permalink
feat: expose bodies and queries
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Aug 27, 2024
1 parent b691125 commit 14705ba
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-donkeys-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kopflos-cms/core": patch
---

More precise typing of core interfaces
6 changes: 6 additions & 0 deletions .changeset/spotty-steaks-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@kopflos-cms/express": patch
"@kopflos-cms/core": patch
---

Adding support for accessing request body
10 changes: 6 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export type { KopflosResponse } from './lib/Kopflos.js'
export type { Kopflos, KopflosConfig } from './lib/Kopflos.js'
export type { Kopflos, KopflosConfig, Body, Query } from './lib/Kopflos.js'
export { default } from './lib/Kopflos.js'
export { loadHandler as defaultHandlerLookup } from './lib/handler.js'
export type { ResourceLoader } from './lib/resourceLoader.js'
export type { Handler, SubjectHandler, ObjectHandler } from './lib/handler.js'
export type { Handler, SubjectHandler, ObjectHandler, HandlerArgs } from './lib/handler.js'
export { default as log, logCode } from './lib/log.js'
export type { KopflosEnvironment } from './lib/env/index.js'
32 changes: 23 additions & 9 deletions packages/core/lib/Kopflos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'node:http'
import type { DatasetCore, NamedNode, Stream } from '@rdfjs/types'
import type { parse } from 'node:querystring'
import type { ReadableStream } from 'node:stream/web'
import type { DatasetCore, NamedNode, Stream, Term } from '@rdfjs/types'
import type { GraphPointer, MultiPointer } from 'clownface'
import type { Options as EndpointOptions, StreamClient } from 'sparql-http-client/StreamClient.js'
import type { ParsingClient } from 'sparql-http-client/ParsingClient.js'
Expand All @@ -17,10 +19,23 @@ import { loadHandler } from './handler.js'
import type { HttpMethod } from './httpMethods.js'
import log from './log.js'

interface KopflosRequest {
type Dataset = ReturnType<KopflosEnvironment['dataset']>

export interface Body<D extends DatasetCore = Dataset> {
quadStream: Stream
dataset: Promise<D>
pointer(): Promise<GraphPointer<NamedNode, D>>
raw: ReadableStream
}

export type Query = ReturnType<typeof parse>

interface KopflosRequest<D extends DatasetCore = DatasetCore> {
iri: NamedNode
method: HttpMethod
headers: IncomingHttpHeaders
body: Body<D> | undefined
query: Query
}

type ResultBody = Stream | DatasetCore | GraphPointer | Error
Expand All @@ -31,10 +46,10 @@ export interface ResultEnvelope {
}
export type KopflosResponse = ResultBody | ResultEnvelope

export interface Kopflos {
export interface Kopflos<D extends DatasetCore = Dataset> {
get env(): KopflosEnvironment
get apis(): MultiPointer
handleRequest(req: KopflosRequest): Promise<ResultEnvelope>
get apis(): MultiPointer<Term, D>
handleRequest(req: KopflosRequest<D>): Promise<ResultEnvelope>
}

interface Clients {
Expand All @@ -57,8 +72,6 @@ export interface Options {
handlerLookup?: HandlerLookup
}

type Dataset = ReturnType<KopflosEnvironment['dataset']>

export default class Impl implements Kopflos {
readonly dataset: Dataset
readonly env: KopflosEnvironment
Expand All @@ -83,11 +96,11 @@ export default class Impl implements Kopflos {
return this.env.clownface({ dataset: this.dataset })
}

get apis(): MultiPointer {
get apis(): MultiPointer<Term, Dataset> {
return this.graph.has(this.env.ns.rdf.type, this.env.ns.kopflos.Api)
}

async handleRequest(req: KopflosRequest): Promise<ResultEnvelope> {
async handleRequest(req: KopflosRequest<Dataset>): Promise<ResultEnvelope> {
const result = await responseOr(this.findResourceShape(req.iri), (resourceShapeMatch: ResourceShapeMatch) => {
const resourceShape = this.graph.node(resourceShapeMatch.resourceShape)

Expand All @@ -99,6 +112,7 @@ export default class Impl implements Kopflos {
dataset: await this.env.dataset().import(coreRepresentation),
})
const args: HandlerArgs = {
...req,
resourceShape,
env: this.env,
subject: resourceGraph.node(resourceShapeMatch.subject),
Expand Down
24 changes: 15 additions & 9 deletions packages/core/lib/handler.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import type { AnyPointer, GraphPointer } from 'clownface'
import type { NamedNode } from '@rdfjs/types'
import type { DatasetCore, NamedNode } from '@rdfjs/types'
import type { KopflosEnvironment } from './env/index.js'
import type { Kopflos, KopflosResponse } from './Kopflos.js'
import type { Kopflos, KopflosResponse, Body, Query } from './Kopflos.js'
import type { ResourceShapeMatch } from './resourceShape.js'
import type { HttpMethod } from './httpMethods.js'
import { logCode } from './log.js'

export interface HandlerArgs {
resourceShape: GraphPointer
type Dataset = ReturnType<KopflosEnvironment['dataset']>

export interface HandlerArgs<D extends DatasetCore = Dataset> {
resourceShape: GraphPointer<NamedNode, D>
env: KopflosEnvironment
subject: GraphPointer
subject: GraphPointer<NamedNode, D>
property: NamedNode | undefined
object: GraphPointer | undefined
object: GraphPointer<NamedNode, D> | undefined
body: Body<D> | undefined
query: Query
}

export interface SubjectHandler {
Expand Down Expand Up @@ -54,9 +58,11 @@ function matchingMethod(env: KopflosEnvironment, requestMethod: HttpMethod): Par
}

return (path: GraphPointer, _, pointers) => {
const handlerMethod = path.out(env.ns.kopflos.method).value?.toUpperCase()
const handlerMethods = path.out(env.ns.kopflos.method).values.map(v => v.toUpperCase())

return handlerMethod === requestMethod ||
(handlerMethod === 'GET' && requestMethod === 'HEAD' && !headHandlerExists(pointers))
return handlerMethods.some(handlerMethod => {
return handlerMethod === requestMethod ||
(handlerMethod === 'GET' && requestMethod === 'HEAD' && !headHandlerExists(pointers))
})
}
}
82 changes: 81 additions & 1 deletion packages/core/test/lib/Kopflos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'mocha-chai-rdf/snapshots.js'
import rdf from '@zazuko/env-node'
import type { Stream } from '@rdfjs/types'
import sinon from 'sinon'
import type { KopflosConfig } from '../../lib/Kopflos.js'
import type { KopflosConfig, Body } from '../../lib/Kopflos.js'
import Kopflos from '../../lib/Kopflos.js'
import { ex } from '../../../testing-helpers/ns.js'
import type { ResourceShapeObjectMatch } from '../../lib/resourceShape.js'
Expand Down Expand Up @@ -46,6 +46,8 @@ describe('lib/Kopflos', () => {
iri: ex.foo,
method: 'GET',
headers: {},
body: undefined,
query: {},
})

// then
Expand All @@ -70,12 +72,80 @@ describe('lib/Kopflos', () => {
iri: ex.foo,
method: 'GET',
headers: {},
body: undefined,
query: {},
})

// then
expect(response).toMatchSnapshot()
})

context('body', () => {
it('can be undefined', async function () {
// given
const kopflos = new Kopflos(config, {
dataset: this.rdf.dataset,
resourceShapeLookup: async () => [{
api: ex.api,
resourceShape: ex.FooShape,
subject: ex.foo,
}],
handlerLookup: async () => ({ body }) => {
return {
status: 200,
body: JSON.stringify({ body: !!body }),
}
},
resourceLoaderLookup: async () => () => rdf.dataset().toStream(),
})

// when
const response = await kopflos.handleRequest({
iri: ex.foo,
method: 'GET',
headers: {},
body: undefined,
query: {},
})

// then
expect(response.body).to.deep.eq('{"body":false}')
})

it('is forwarded to handler when defined', async function () {
// given
const kopflos = new Kopflos(config, {
dataset: this.rdf.dataset,
resourceShapeLookup: async () => [{
api: ex.api,
resourceShape: ex.FooShape,
subject: ex.foo,
}],
handlerLookup: async () => ({ body }) => {
return {
status: 200,
body: JSON.stringify({ body: !!body }),
}
},
resourceLoaderLookup: async () => () => rdf.dataset().toStream(),
})

// when
const response = await kopflos.handleRequest({
iri: ex.foo,
method: 'GET',
headers: {},
body: {

} as unknown as Body,
query: {},
})

// then
expect(response.body).to.deep.eq('{"body":true}')
})
})

context('when no handler is found', () => {
for (const method of ['GET', 'HEAD'] as const) {
context('when method is ' + method, () => {
Expand All @@ -96,6 +166,8 @@ describe('lib/Kopflos', () => {
iri: ex.foo,
method,
headers: {},
body: undefined,
query: {},
}) as unknown as { status: number; body: Stream }

// then
Expand Down Expand Up @@ -125,6 +197,8 @@ describe('lib/Kopflos', () => {
iri: ex.foo,
method,
headers: {},
body: undefined,
query: {},
})

// then
Expand Down Expand Up @@ -155,6 +229,8 @@ describe('lib/Kopflos', () => {
iri: ex.baz,
method: 'GET',
headers: {},
body: undefined,
query: {},
})

// then
Expand Down Expand Up @@ -182,6 +258,8 @@ describe('lib/Kopflos', () => {
iri: ex.baz,
method: 'GET',
headers: {},
body: undefined,
query: {},
})

// then
Expand Down Expand Up @@ -211,6 +289,8 @@ describe('lib/Kopflos', () => {
iri: ex.baz,
method,
headers: {},
body: undefined,
query: {},
})

// then
Expand Down
36 changes: 36 additions & 0 deletions packages/express/BodyWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Readable } from 'node:stream'
import type { Request } from 'express'
import type { NamedNode } from '@rdfjs/types'
import type { Body } from '@kopflos-cms/core'
import type { Environment } from '@rdfjs/environment/Environment.js'
import type ClownfaceFactory from 'clownface/Factory.js'
import type { Dataset } from '@zazuko/env/lib/DatasetExt.js'
import type { DatasetFactoryExt } from '@zazuko/env/lib/DatasetFactoryExt.js'
import onetime from 'onetime'

export class BodyWrapper implements Body {
declare dataset: Promise<Dataset>

constructor(private readonly env: Environment<DatasetFactoryExt | ClownfaceFactory>, private readonly term: NamedNode, private readonly req: Readable & Pick<Request, 'quadStream'>) {
Object.defineProperty(this, 'dataset', {
get: onetime(() => {
this.env.dataset().import(this.quadStream)
}),
})
}

get quadStream() {
return this.req.quadStream!()
}

async pointer() {
return this.env.clownface({
dataset: await this.dataset,
term: this.term,
})
}

get raw() {
return Readable.toWeb(this.req)
}
}
Loading

0 comments on commit 14705ba

Please sign in to comment.