API
List objects and measure usage on the server and in the browser.
Server
With the plugin registered, s3.db.objects is available in routes, quota
checks, and jobs:
const scope = "user:123";
const ref = { bucket: "my-bucket", key: "avatar/photo.jpg" };
const objects = await s3.db.objects.listByScope({
scope,
status: "active",
route: "avatar",
limit: 50,
cursor: previous.nextCursor ?? undefined,
});
const count = await s3.db.objects.countByScope(scope, "active");
const usage = await s3.db.objects.getScopeUsage(scope);
const object = await s3.db.objects.find({ bucket: ref.bucket, key: ref.key });
const active = await s3.db.objects.findActive({
bucket: ref.bucket,
key: ref.key,
});
const pending = await s3.db.objects.findPendingMultipart({
bucket: ref.bucket,
key: ref.key,
fileSize: 1024000,
});
const written = await s3.db.objects.upsertPending({
scope,
bucket: ref.bucket,
key: ref.key,
route: "avatar",
declaredSize: 1024000,
});
const active = await s3.db.objects.markActive({
bucket: ref.bucket,
key: ref.key,
size: 1024000,
});StorageObjectStore
import type { StorageObjectStore } from "@dimah-s3/db";Prop
Type
To remove a file, use api.delete — not store helpers. See
delete behavior.
GET /db/objects (browser listObjects) defaults to status: active and
limit: 50, and rejects a limit above 100. It returns nextCursor
when another page exists. GET /db/object (getObject) loads one row by
key. Pass limit on listByScope yourself for server listings.
Browser
Register dbClient() once. In a client component, s3Client.useApi() gives
you api.db.listObjects. Call it and render the list yourself — there is no
list hook or list component.
"use client";
import { createS3Client } from "@dimah-s3/react";
import { dbClient } from "@dimah-s3/db/client";
export const s3Client = createS3Client({
basePath: "/api/s3",
plugins: [dbClient()],
});"use client";
import { useCallback, useEffect, useState } from "react";
import type { DbClientObject } from "@dimah-s3/db/client";
import { s3Client } from "@/lib/s3-client";
export function Files() {
const api = s3Client.useApi();
const [objects, setObjects] = useState<DbClientObject[]>([]);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const load = useCallback(
async (cursor?: string) => {
const page = await api.db.listObjects({
route: "avatar",
...(cursor ? { cursor } : {}),
});
setObjects((prev) =>
cursor ? [...prev, ...page.objects] : page.objects,
);
setNextCursor(page.nextCursor ?? null);
},
[api],
);
useEffect(() => {
void load();
}, [load]);
return (
<>
<ul>
{objects.map((file) => (
<li key={file.id}>{file.filename ?? file.key}</li>
))}
</ul>
{nextCursor ? (
<button type="button" onClick={() => void load(nextCursor)}>
More
</button>
) : null}
</>
);
}Scope is resolved on the server
listObjects is the browser wrapper for GET /db/objects. It does not take a
scope. The plugin uses resolveScope on the request. To list another scope,
call listByScope on the server.
Quota guards
For the normal db() integration, pass app-owned limits through quota. The
plugin runs the check after its ownership guard:
plugins: [
db({
client: dimahS3Db,
resolveScope,
quota: { maxBytes: 100 * 1024 * 1024, maxFiles: 50 },
}),
],The guard projects the scope's current DB usage plus the incoming file's
declared size. It rejects a missing scope with UNAUTHORIZED and an exceeded
byte or file-count limit with QUOTA_EXCEEDED (HTTP 413).
createQuotaGuard
Use createQuotaGuard when you need the same quota check in a route guard you
compose yourself. Its signature is
createQuotaGuard(options): (context: QuotaGuardContext) => Promise<void>.
import { createQuotaGuard } from "@dimah-s3/db";
const quotaGuard = createQuotaGuard({
client: dimahS3Db,
resolveScope,
maxBytes: 100 * 1024 * 1024,
maxFiles: 50,
});
avatar: route({
upload: {
guard: quotaGuard,
},
});When db() is also registered, its plugin guard runs before this route guard.
Use chainHooks(quotaGuard, anotherGuard) from @dimah-s3/server when the
route already has another upload guard.
Prop
Type
Keep a separate quota table or counter updated by your hooks.
getScopeUsage scans every row for the scope on each call, which is suitable
for demos and light use but not frequent presign checks on large scopes.
Object access guards
db() already installs object access guards for its routes. Use
createObjectAccessGuard as an escape hatch when you manually compose a
custom route policy. Its signature is
createObjectAccessGuard(options): (context: ObjectAccessGuardContext) => Promise<void>.
The guard resolves the caller's scope, loads the row by bucket and key, checks
its status and ownership, and optionally verifies a multipart uploadId. It
throws UNAUTHORIZED, OBJECT_NOT_FOUND, or FORBIDDEN when a check fails.
import { createObjectAccessGuard } from "@dimah-s3/db";
const activeObjectGuard = createObjectAccessGuard({
client: dimahS3Db,
resolveScope,
// requireStatus defaults to "active"
});
documents: route({
// This route owns its guard composition; matching DB rows must already exist.
plugins: { db: false },
download: { guard: activeObjectGuard },
delete: { guard: activeObjectGuard },
});The default "active" status fits download and delete. For a new upload's
manual confirm guard use requireStatus: "pending" (an overwrite can use
"any"); for upload.multipart.guard, also set requireUploadId: true.
requireStatus: "any" accepts pending or active rows, never deleted rows.
authorize replaces the default object.scope === scope ownership test. A
custom callback must therefore enforce every ownership or sharing rule your
application requires.
Prop
Type