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. A prefix is applied in the query, so a page
stays full when you filter by key prefix. 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. useObjectList
pages that response. dbObjectQuery sends the open folder id as prefix.
It does not turn keys into folders.
"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 { dbObjectQuery } from "@dimah-s3/db/client";
import { useObjectList } from "@dimah-s3/react";
import { s3Client } from "@/lib/s3-client";
export function Files() {
const api = s3Client.useApi();
const list = useObjectList({
query: dbObjectQuery(api.db.listObjects, { route: "avatar" }),
queryKey: ["avatar"],
});
if (list.isLoading) return <p>Loading…</p>;
return (
<ul>
{list.items.map((file) => (
<li key={file.id}>{file.filename ?? file.key}</li>
))}
{list.hasMore ? (
<li>
<button type="button" onClick={() => void list.loadMore()}>
More
</button>
</li>
) : null}
</ul>
);
}ObjectList renders the same state
if you want the folder chrome. list.usage is the scope total from this
response. Pass the same caps you gave db({ quota }) as usageLimits on
ObjectList. dbObjectListAccessors() supplies getKey,
getObjectKey, getLabel, getFileName, getFileType, and getDescription
for a DbClientObject. Call listObjects yourself when you do not
want the hook. To list another scope, call listByScope on the server.
<Callout title="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.
</Callout>
## Quota guards
For the normal `db()` integration, pass app-owned limits through `quota`. The
plugin runs the check after its ownership guard:
```ts
plugins: [
db({
client: dimahS3Db,
resolveScope,
quota: { maxBytes: 100 * 1024 * 1024, maxFiles: 50 }, // [!code ++]
}),
],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.
operation is passed to resolveScope with the route and key, so create one
guard per hook.
import { createObjectAccessGuard } from "@dimah-s3/db";
const guardFor = (operation: "download" | "delete") =>
createObjectAccessGuard({
client: dimahS3Db,
resolveScope,
operation,
// requireStatus defaults to "active"
});
documents: route({
// This route owns its guard composition; matching DB rows must already exist.
plugins: { db: false },
download: { guard: guardFor("download") },
delete: { guard: guardFor("delete") },
});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