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,
});StorageObjectStore
import type { StorageObjectStore, StorageObject } 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 and extra guards
Quotas are app-owned numbers. Pass them to db({ quota }) or
createQuotaGuard — the plugin only compares usage.
plugins: [
db({
client: dimahS3Db,
resolveScope,
quota: { maxBytes: 100 * 1024 * 1024, maxFiles: 50 },
}),
],Ownership still runs first. Stack more user guards with chainHooks:
getScopeUsage scans every row for the scope on each call. That is fine
for demos and light use. For frequent presign checks or large scopes, keep
a separate quota table (or counter) updated in your hooks instead of
recounting each time.
Custom access
For rules beyond same-scope ownership, add a guard on the feature config
(it runs after the plugin) or use createObjectAccessGuard on your own
routes:
download: {
guard: async ({ request, bucket, key }) => {
// ownership already checked by the db plugin
},
},createObjectAccessGuard({ client: dimahS3Db, resolveScope, authorize: myRule });authorize replaces the default object.scope === scope check. Use it
where you own the full access policy — custom routes, or a guard you wire
yourself.