Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 7 additions & 5 deletions reference/database/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
kriszyp marked this conversation as resolved.
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);
}
}
```
Expand Down
5 changes: 3 additions & 2 deletions reference/database/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
kriszyp marked this conversation as resolved.
});
Expand Down
4 changes: 2 additions & 2 deletions reference/resources/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
}
```
Expand Down
25 changes: 13 additions & 12 deletions reference/resources/resource-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 };
}
Expand All @@ -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 });
}
Expand All @@ -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).

Expand Down
Loading