diff --git a/packages/api/DittoApp.test.ts b/packages/api/DittoApp.test.ts new file mode 100644 index 00000000..b6525eed --- /dev/null +++ b/packages/api/DittoApp.test.ts @@ -0,0 +1,16 @@ +import { Hono } from '@hono/hono'; + +import { DittoApp } from './DittoApp.ts'; +import { DittoRoute } from './DittoRoute.ts'; + +Deno.test('DittoApp', () => { + const app = new DittoApp(); + + const hono = new Hono(); + const route = new DittoRoute(); + + app.route('/', route); + + // @ts-expect-error Passing a non-DittoRoute to route. + app.route('/', hono); +}); diff --git a/packages/api/DittoApp.ts b/packages/api/DittoApp.ts new file mode 100644 index 00000000..3e154e17 --- /dev/null +++ b/packages/api/DittoApp.ts @@ -0,0 +1,8 @@ +import { Hono } from '@hono/hono'; + +import type { DittoEnv } from './DittoEnv.ts'; + +export class DittoApp extends Hono { + // @ts-ignore Require a DittoRoute for type safety. + declare route: (path: string, app: Hono) => Hono; +} diff --git a/packages/api/DittoEnv.ts b/packages/api/DittoEnv.ts new file mode 100644 index 00000000..d82ed911 --- /dev/null +++ b/packages/api/DittoEnv.ts @@ -0,0 +1,17 @@ +import type { DittoConf } from '@ditto/conf'; +import type { DittoDatabase } from '@ditto/db'; +import type { Env } from '@hono/hono'; +import type { NRelay } from '@nostrify/nostrify'; + +export interface DittoEnv extends Env { + Variables: { + /** Ditto site configuration. */ + conf: DittoConf; + /** Main database. */ + store: NRelay; + /** Database object. */ + db: DittoDatabase; + /** Abort signal for the request. */ + signal: AbortSignal; + }; +} diff --git a/packages/api/DittoRoute.test.ts b/packages/api/DittoRoute.test.ts new file mode 100644 index 00000000..737019c4 --- /dev/null +++ b/packages/api/DittoRoute.test.ts @@ -0,0 +1,12 @@ +import { assertEquals } from '@std/assert'; + +import { DittoRoute } from './DittoRoute.ts'; + +Deno.test('DittoRoute', async () => { + const route = new DittoRoute(); + const response = await route.request('/'); + const body = await response.json(); + + assertEquals(response.status, 500); + assertEquals(body, { error: 'Missing required variable: db' }); +}); diff --git a/packages/api/DittoRoute.ts b/packages/api/DittoRoute.ts index cebbc06a..d1b06a86 100644 --- a/packages/api/DittoRoute.ts +++ b/packages/api/DittoRoute.ts @@ -1,52 +1,59 @@ -import { type Env, Hono } from '@hono/hono'; +import { type Context, type ErrorHandler, Hono } from '@hono/hono'; +import { HTTPException } from '@hono/hono/http-exception'; -import type { DittoConf } from '@ditto/conf'; -import type { DittoDatabase, DittoTables } from '@ditto/db'; import type { HonoOptions } from '@hono/hono/hono-base'; -import type { NostrSigner, NPool, NRelay, NStore, NUploader } from '@nostrify/nostrify'; -import type { Kysely } from 'kysely'; - -interface DittoEnv extends Env { - Variables: { - conf: DittoConf; - user?: { - /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ - signer: NostrSigner; - /** Storage for the user, might filter out unwanted content. */ - store: NStore; - }; - /** Uploader for the user to upload files. */ - uploader?: NUploader; - /** Kysely instance for the database. */ - kysely: Kysely; - /** Main database. */ - store: NRelay; - /** Internal Nostr relay for realtime subscriptions. */ - pubsub: NRelay; - /** Nostr relay pool. */ - pool: NPool; - /** Database object. */ - db: DittoDatabase; - /** Normalized pagination params. */ - pagination: { since?: number; until?: number; limit: number }; - /** Normalized list pagination params. */ - listPagination: { offset: number; limit: number }; - /** Translation service. */ - translator?: DittoTranslator; - signal: AbortSignal; - pipeline: Pick; - }; -} +import type { DittoEnv } from './DittoEnv.ts'; +/** + * Ditto base route class. + * Ensures that required variables are set for type safety. + */ export class DittoRoute extends Hono { constructor(opts: HonoOptions = {}) { super(opts); - this.init(); - } - init(): void { this.use((c, next) => { + this.setSignal(c); + this.assertVars(c.var); return next(); }); + + this.onError(this._errorHandler); } + + private setSignal(c: Context): void { + if (!c.var.signal) { + c.set('signal', c.req.raw.signal); + } + } + + private assertVars(vars: Partial): DittoEnv['Variables'] { + if (!vars.db) this.throwMissingVar('db'); + if (!vars.conf) this.throwMissingVar('conf'); + if (!vars.store) this.throwMissingVar('store'); + if (!vars.signal) this.throwMissingVar('signal'); + + return { + db: vars.db, + conf: vars.conf, + store: vars.store, + signal: vars.signal, + }; + } + + private throwMissingVar(name: string): never { + throw new HTTPException(500, { message: `Missing required variable: ${name}` }); + } + + private _errorHandler: ErrorHandler = (error, c) => { + if (error instanceof HTTPException) { + if (error.res) { + return error.res; + } else { + return c.json({ error: error.message }, error.status); + } + } + + return c.json({ error: 'Something went wrong' }, 500); + }; } diff --git a/packages/api/deno.json b/packages/api/deno.json index a8bbb3f5..f9befe4b 100644 --- a/packages/api/deno.json +++ b/packages/api/deno.json @@ -2,6 +2,7 @@ "name": "@ditto/api", "version": "1.1.0", "exports": { + ".": "./mod.ts", "./middleware": "./middleware/mod.ts" } } diff --git a/packages/api/mod.ts b/packages/api/mod.ts index ee8dc644..8e9d1d46 100644 --- a/packages/api/mod.ts +++ b/packages/api/mod.ts @@ -1 +1,4 @@ +export { DittoApp } from './DittoApp.ts'; export { DittoRoute } from './DittoRoute.ts'; + +export type { DittoEnv } from './DittoEnv.ts';