# API (https://v1.s3.dimah.dev/docs/db/api)

## Server [#server]

With the plugin registered, `s3.db.objects` is available in routes, quota
checks, and jobs:

```ts
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 [#storageobjectstore]

```ts
import type { StorageObjectStore } from "@dimah-s3/db";
```

<AutoTypeTable
  path="packages/db/src/store/storage-object-store.ts"
  name="StorageObjectStore"
/>

To remove a file, use [`api.delete`](https://v1.s3.dimah.dev/docs/react) — not store helpers. See
[delete behavior](https://v1.s3.dimah.dev/docs/db/setup#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 [#browser]

Register `dbClient()` once. In a client component, `s3Client.useApi()` gives
you `api.db.listObjects`. [`useObjectList`](https://v1.s3.dimah.dev/docs/react/hooks/object-list)
pages that response. `dbObjectQuery` sends the open folder id as `prefix`.
It does not turn keys into folders.

```ts title="lib/s3-client.ts"
"use client";

import { createS3Client } from "@dimah-s3/react";
import { dbClient } from "@dimah-s3/db/client"; // [!code ++]

export const s3Client = createS3Client({
  basePath: "/api/s3",
  plugins: [dbClient()], // [!code ++]
});
```

```tsx title="components/file-list.tsx"
"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`](https://v1.s3.dimah.dev/docs/react/ui/components/object-list) 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 [#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>`.

```ts
import { createQuotaGuard } from "@dimah-s3/db";

const quotaGuard = createQuotaGuard({
  client: dimahS3Db,
  resolveScope,
  maxBytes: 100 * 1024 * 1024, // [!code ++]
  maxFiles: 50, // [!code ++]
});

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.

<AutoTypeTable
  path="packages/db/src/hooks/create-quota-guard.ts"
  name="CreateQuotaGuardOptions"
/>

<Accordions>
<Accordion title="How should I check quotas for a large scope?">

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.

</Accordion>
</Accordions>

## Object access guards [#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.

```ts
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 }, // [!code ++]
  download: { guard: guardFor("download") }, // [!code ++]
  delete: { guard: guardFor("delete") }, // [!code ++]
});
```

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.

<AutoTypeTable
  path="packages/db/src/hooks/create-object-access-guard.ts"
  name="CreateObjectAccessGuardOptions"
/>
