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

feat: handler chains #179

Merged
merged 7 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions .changeset/slimy-frogs-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kopflos-cms/core": patch
---

Object of `kl:handler` can now be an RDF List of handler implementations which will be called in sequence
4 changes: 2 additions & 2 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type { KopflosResponse } from './lib/Kopflos.js'
export type { KopflosResponse, ResultEnvelope } 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 { loadHandlers as defaultHandlerLookup } from './lib/handler.js'
export type { ResourceLoader } from './lib/resourceLoader.js'
export type { Handler, SubjectHandler, ObjectHandler, HandlerArgs } from './lib/handler.js'
export { default as log, logCode } from './lib/log.js'
Expand Down
39 changes: 29 additions & 10 deletions packages/core/lib/Kopflos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { isResponse } from './isResponse.js'
import type { ResourceLoader, ResourceLoaderLookup } from './resourceLoader.js'
import { insertShorthands, fromOwnGraph, findResourceLoader } from './resourceLoader.js'
import type { Handler, HandlerArgs, HandlerLookup } from './handler.js'
import { loadHandler } from './handler.js'
import { loadHandlers } from './handler.js'
import type { HttpMethod } from './httpMethods.js'
import log from './log.js'

Expand Down Expand Up @@ -44,6 +44,7 @@ export interface ResultEnvelope {
body?: ResultBody | string
status?: number
headers?: OutgoingHttpHeaders
end?: boolean
}
export type KopflosResponse = ResultBody | ResultEnvelope

Expand Down Expand Up @@ -118,9 +119,9 @@ export default class Impl implements Kopflos {
return loader
}
const coreRepresentation = loader(resourceShapeMatch.subject, this)
const handler = await this.loadHandler(req.method, resourceShapeMatch, coreRepresentation)
if (isResponse(handler)) {
return handler
const handlerChain = await this.loadHandlerChain(req.method, resourceShapeMatch, coreRepresentation)
if (isResponse(handlerChain)) {
return handlerChain
}
const resourceGraph = this.env.clownface({
dataset: await this.env.dataset().import(coreRepresentation),
Expand All @@ -138,7 +139,15 @@ export default class Impl implements Kopflos {
args.property = resourceShapeMatch.property
args.object = resourceGraph.node(resourceShapeMatch.object)
}
return handler(args)
const [handler, ...rest] = handlerChain
let response = this.asEnvelope(await handler(args, undefined))
for (const handler of rest) {
response = this.asEnvelope(await handler(args, response))
if (response.end) {
break
}
}
return response
}

async handleRequest(req: KopflosRequest<Dataset>): Promise<ResultEnvelope> {
Expand Down Expand Up @@ -225,13 +234,13 @@ export default class Impl implements Kopflos {
return fromOwnGraph
}

async loadHandler(method: HttpMethod, resourceShapeMatch: ResourceShapeMatch, coreRepresentation: Stream): Promise<Handler | KopflosResponse> {
const handlerLookup = this.options.handlerLookup || loadHandler
async loadHandlerChain(method: HttpMethod, resourceShapeMatch: ResourceShapeMatch, coreRepresentation: Stream): Promise<Handler[] | KopflosResponse> {
const handlerLookup = this.options.handlerLookup || loadHandlers

const handler = await handlerLookup(resourceShapeMatch, method, this)
const handlers = await Promise.all(handlerLookup(resourceShapeMatch, method, this))

if (handler) {
return handler
if (handlers.length) {
return handlers
}

if (!('property' in resourceShapeMatch) && (method === 'GET' || method === 'HEAD')) {
Expand All @@ -250,6 +259,16 @@ export default class Impl implements Kopflos {
return 'body' in arg || 'status' in arg
}

private asEnvelope(arg: KopflosResponse): ResultEnvelope {
if (this.isEnvelope(arg)) {
return arg
}
return {
status: 200,
body: arg,
}
}

static async fromGraphs(kopflos: Impl, ...graphs: Array<NamedNode | string>): Promise<void> {
const graphsIris = graphs.map(graph => typeof graph === 'string' ? kopflos.env.namedNode(graph) : graph)
log.info('Loading graphs', graphsIris.map(g => g.value))
Expand Down
25 changes: 19 additions & 6 deletions packages/core/lib/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { AnyPointer, GraphPointer } from 'clownface'
import type { DatasetCore, NamedNode } from '@rdfjs/types'
import type { KopflosEnvironment } from './env/index.js'
import type { Kopflos, KopflosResponse, Body, Query } from './Kopflos.js'
import type { Kopflos, KopflosResponse, Body, Query, ResultEnvelope } from './Kopflos.js'
import type { ResourceShapeMatch } from './resourceShape.js'
import type { HttpMethod } from './httpMethods.js'
import { logCode } from './log.js'
Expand All @@ -21,20 +21,20 @@
}

export interface SubjectHandler {
(arg: HandlerArgs): KopflosResponse | Promise<KopflosResponse>
(arg: HandlerArgs, response?: ResultEnvelope): KopflosResponse | Promise<KopflosResponse>
}

export interface ObjectHandler {
(arg: Required<HandlerArgs>): KopflosResponse | Promise<KopflosResponse>
(arg: Required<HandlerArgs>, response?: ResultEnvelope): KopflosResponse | Promise<KopflosResponse>
}

export type Handler = SubjectHandler | ObjectHandler

export interface HandlerLookup {
(match: ResourceShapeMatch, method: HttpMethod, kopflos: Kopflos): Promise<Handler | undefined> | Handler | undefined
(match: ResourceShapeMatch, method: HttpMethod, kopflos: Kopflos): Array<Promise<Handler> | Handler>
}

export function loadHandler({ resourceShape, ...rest }: ResourceShapeMatch, method: HttpMethod, { apis, env }: Kopflos) {
export const loadHandlers: HandlerLookup = ({ resourceShape, ...rest }: ResourceShapeMatch, method: HttpMethod, { apis, env }: Kopflos) => {
const api = apis.node(rest.api)

let shape: AnyPointer = api.node(resourceShape)
Expand All @@ -50,8 +50,21 @@
.filter(matchingMethod(env, method))

const impl = handler.out(env.ns.code.implementedBy)
if (impl.isList()) {
const pointers = [...impl.list()]
return pointers.map(chainedHandler => {
logCode(chainedHandler, 'handler')
return env.load<Handler>(chainedHandler)
}).filter(Boolean) as Array<Promise<Handler>>
}

Check warning on line 59 in packages/core/lib/handler.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/lib/handler.ts#L54-L59

Added lines #L54 - L59 were not covered by tests

logCode(impl, 'handler')
return env.load<Handler>(impl)
const loaded = env.load<Handler>(impl)
if (loaded) {
return [loaded]
}

return []
}

function matchingMethod(env: KopflosEnvironment, requestMethod: HttpMethod): Parameters<AnyPointer['filter']>[0] {
Expand Down
Loading