dimah-s3v0.4.1
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: "uploads/photo.jpg" };

const objects = await s3.db.objects.listByScope({
  scope,
  status: "active",
  limit: 50,
  offset: 0,
});

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,
});
MethodPurpose
listByScopeList for a scope — skips deleted by default
countByScopeCount with the same filters as listByScope
getScopeUsageTotal bytes and count — quota checks
findOne row by bucket + key (any status)
findActiveOne row — only active
findPendingMultipartMultipart resume lookup

To remove a file, use api.delete — not store helpers (Setup).

Browser (client-side)

Register dbClient() once. In a client component, useApi() gives you api.db.listObjects:

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

export const { api, S3Provider, useApi } = createS3Client({
  basePath: "/api/s3",
  plugins: [dbClient()],
});
components/file-list.tsx
"use client";

import { useEffect, useState } from "react";
import { useApi } from "@/lib/s3-client";

export function FileList() {
  const api = useApi();
  const [files, setFiles] = useState([]);

  useEffect(() => {
    api.db
      .listObjects({ status: "active", limit: 50, offset: 0 })
      .then((result) => setFiles(result.objects));
  }, [api]);

  return (
    <ul>
      {files.map((file) => (
        <li key={file.id}>{file.filename ?? file.key}</li>
      ))}
    </ul>
  );
}

listObjects is browser-only. (scope is a server argument and in listObjects automatically resolved by the plugin in the server).

Quota and extra guards

Quotas are app-owned. The db plugin enforces ownership first; your guards run after (Composition).

import { dimahS3, chainHooks } from "@dimah-s3/server";
import { db } from "@dimah-s3/db";

export const s3 = dimahS3({
  plugins: [db({ client: dimahS3Db, resolveScope })],
  upload: {
    enabled: true,
    presignGuard: quotaGuard,
    // or stack several: chainHooks(quotaGuard, rateLimitGuard),
  },
});
async function quotaGuard({ request, fileSize }) {
  const scope = await resolveScope(request);
  const { totalBytes } = await s3.db.objects.getScopeUsage(scope);
  if (totalBytes + (fileSize ?? 0) > MAX_BYTES)
    throw forbidden("Quota exceeded");
}

getScopeUsage scans every row for the scope on each call — 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.

Use chainHooks when you need more than one user guard on the same hook. Ownership stays on the plugin — you only add policy on top.

Custom access

For rules beyond same-scope ownership, add a guard on the feature config (runs after the plugin) or use createObjectAccessGuard on your own routes:

download: {
  enabled: true,
  presignGuard: async ({ request, bucket, key }) => {
    // ownership already checked by the db plugin
  },
},
createObjectAccessGuard({ db: 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