diff --git a/reference/database/api.md b/reference/database/api.md index 17d71b77..6197ec4a 100644 --- a/reference/database/api.md +++ b/reference/database/api.md @@ -249,19 +249,21 @@ await Photo.put({ id, data: blob }); ### Accepting Binary in JSON Requests -REST clients that can't post raw binary typically send base64 inside JSON. Decode in the resource override and wrap with `createBlob`, recording the MIME type so it round-trips on read: +REST clients that can't post raw binary typically send base64 inside JSON. Decode in the resource override and wrap with `createBlob`, recording the MIME type so it round-trips on read. The `record` parameter is a promise for the request body, so `await` it once before inspecting it — reading fields off the unresolved promise silently yields `undefined` and stores the raw base64 string: ```typescript import { type RequestTargetOrId, tables, createBlob } from 'harper'; export class Photo extends tables.Photo { static async post(target: RequestTargetOrId, record: any) { - if (record.data) { - record.data = createBlob(Buffer.from(record.data, record.encoding || 'base64'), { - type: record.contentType || 'application/octet-stream', + const body = await record; + if (!body) return new Response('A JSON body is required', { status: 400 }); + if (body.data) { + body.data = createBlob(Buffer.from(body.data, body.encoding || 'base64'), { + type: body.contentType || 'application/octet-stream', }); } - return super.post(target, record); + return super.post(target, body); } } ``` diff --git a/reference/database/schema.md b/reference/database/schema.md index ffadf06e..ef96ecd9 100644 --- a/reference/database/schema.md +++ b/reference/database/schema.md @@ -866,8 +866,9 @@ When returning a blob via REST, register an error handler to handle interrupted ```javascript export class MyEndpoint extends MyTable { static async get(target) { - const record = super.get(target); - let blob = record.data; + const record = await super.get(target); + let blob = record?.data; + if (!blob) return record; blob.on('error', () => { MyTable.invalidate(target); }); diff --git a/reference/resources/overview.md b/reference/resources/overview.md index d21b74d6..d0574af6 100644 --- a/reference/resources/overview.md +++ b/reference/resources/overview.md @@ -55,8 +55,8 @@ export class MyTable extends tables.MyTable { } static async post(target, data) { - // custom action on POST - this.create({ ...(await data), status: 'pending' }); + // custom action on POST; return the write so the response waits for the commit + return this.create({ ...(await data), status: 'pending' }); } } ``` diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 1f121d54..b64c6e7d 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -707,6 +707,8 @@ Returns the current context, which includes: - `user` — User object with username, role, and authorization information - `transaction` — The current transaction +`getContext()` is a Resource instance method. A static verb is not an instance, so it reads the context from the trailing argument the request path passes it — `(target, context)` for `get`/`delete`, `(target, data, context)` for `put`/`patch`/`post`. A direct server-side call supplies that argument only if the caller passes one, so use the [`getContext()` export](#getcontext-context-1) from the `harper` module when a static method must work on both paths. + When triggered by HTTP, the context is the `Request` object with these additional properties: - `url` — Full local path including query string @@ -731,11 +733,11 @@ Executes a Harper operations API call using this table as the target. Set `autho ### `getCurrentUser(): User | undefined` -Returns the user associated with the current request, or `undefined` if no user is authenticated. The returned object exposes the username, role, and `role.permission` flags. +Returns the user associated with the current request, or `undefined` if no user is authenticated. The returned object exposes the username, role, and `role.permission` flags. This is a Resource **instance** method; a static verb reads the same user from the context it is passed, as `context.user`: ```javascript -async get(target) { - const user = this.getCurrentUser(); +static async get(_target, context) { + const user = context?.user; if (!user) return new Response(null, { status: 401 }); return { username: user.username, role: user.role }; } @@ -745,14 +747,14 @@ async get(target) { ### Session and Login from a Resource -The context returned by `getContext()` exposes `login` and `session` for handling sign-in/out flows in a custom Resource. Sessions require `authentication.enableSessions: true` in `harper-config.yaml`. +The request context exposes `login` and `session` for handling sign-in/out flows in a custom Resource. A static verb receives it as its trailing argument — `(target, data, context)` for `post`. Sessions require `authentication.enableSessions: true` in `harperdb-config.yaml`. ```typescript export class SignIn extends Resource { - async post(_target, data) { - const context = this.getContext(); + static async post(_target, data, context) { + const { username, password } = (await data) ?? {}; try { - await context.login(data.username, data.password); + await context.login(username, password); } catch { return new Response('Invalid credentials', { status: 403 }); } @@ -761,16 +763,15 @@ export class SignIn extends Resource { } export class SignOut extends Resource { - async post() { - const context = this.getContext(); - if (!context.session) return new Response(null, { status: 401 }); - await context.session.delete(context.session.id); + static async post(_target, _data, context) { + if (!context?.session?.user) return new Response(null, { status: 401 }); + await context.session.update({ user: null }); return new Response('Logged out', { status: 200 }); } } ``` -`context.login(username, password)` verifies credentials and establishes the session cookie on success. To end a session, delete it via `context.session.delete(context.session.id)`. +`context.login(username, password)` verifies credentials and establishes the session cookie on success. To end a session, clear its user with `context.session.update({ user: null })` — [`update`](../http/api.md#properties) is the session's only mutator. `context.session` is an empty object rather than `undefined` when sessions are enabled and the request carries no session cookie, so test `context?.session?.user` to detect an established session. Cookie-based sessions are intended for browser clients. For non-browser clients (CLI tools, mobile apps, service-to-service), use JWT issuance — see [JWT Authentication](../security/jwt-authentication.md).