dimah-s3v1.5.5
Database

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.

lib/s3-client.ts
"use client";

import { createS3Client } from "@dimah-s3/react";
import { dbClient } from "@dimah-s3/db/client";

export const s3Client = createS3Client({
  basePath: "/api/s3",
  plugins: [dbClient()],
});
components/file-list.tsx
"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:

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.

On this page