npm package: @rpcbase/db
Description: This package provides a small Zod extension that adds `.unique()` and `.sparse()` to `z.string()`, `z.number()`, and `z.date()` (for chaining/compatibility).
Latest version: 0.89.0

## `@rpcbase/db`

### Zod extensions

This package provides a small Zod extension that adds `.unique()` and `.sparse()` to `z.string()`, `z.number()`, and `z.date()` (for chaining/compatibility).

```ts
import { z } from "@rpcbase/db"

const ZUser = z.object({
  email: z.string().email().unique().sparse(),
  createdAt: z.date().sparse(),
})
```

### `LocalizedString` / `zI18nString()`

`zI18nString()` (alias of `zLocalizedString()`) describes a localized string as an object keyed by *language codes* (BCP47):

```ts
import { z, zI18nString } from "@rpcbase/db"

const ZPost = z.object({
  title: zI18nString(), // { "en": "...", "fr": "...", "fr-FR": "..." }
})
```

### Mongoose re-export

```ts
import { Schema, model, mongoose } from "@rpcbase/db"
```

### Tenant model context (`models.get` / `models.getUnsafe`)

Tenant-scoped model loading supports both context shapes:

```ts
import { models } from "@rpcbase/db"

const ItemA = await models.get("Item", { tenantId: "tenant-1" })
const ItemB = await models.get("Item", {
  req: { session: { user: { currentTenantId: "tenant-1" } } },
})
```

`ctx.tenantId` is checked first, then fallback is `ctx.req.session.user.currentTenantId`.

### Transactions (MongoDB / Mongoose sessions)

`@rpcbase/db` supports MongoDB transactions via Mongoose sessions.

To make a write atomic across **tenant DB + global DB + tenant filesystem DB**, use `withTransaction(...)` and pass `{ session }` to **every** operation (Mongoose and native driver):

```ts
import { models, withTransaction } from "@rpcbase/db"

await withTransaction({ tenantId }, async ({ session, ctx, db }) => {
  const Item = await models.get("Item", ctx.tenant)
  const item = await Item.create({ uid: "u1", name: "Hello", createdAt: Date.now() }, { session })

  const globalNative = db.global.db
  if (globalNative) {
    await globalNative.collection("rb_audit_log").insertOne({ itemId: item._id.toString() }, { session })
  }

  const filesystemNative = db.filesystem.db
  if (filesystemNative) {
    await filesystemNative.collection("rb_fs_meta").insertOne({ itemId: item._id.toString() }, { session })
  }
})
```

Notes:
- MongoDB transactions require a replica set (or a sharded cluster). On a standalone MongoDB server, `withTransaction` will fail.
- To include deletes in the same transaction, the RTS delete change-log plugin is session-aware (`rtsChangeLogPlugin`).

### Mongoose localized fallback (optional)

When storing localized strings as plain objects, `mongooseLocalizedStringField()` adds a getter that returns a proxy with fallback behavior:

```ts
import { Schema, mongooseLocalizedStringField } from "@rpcbase/db"

const PostSchema = new Schema({
  title: mongooseLocalizedStringField(),
})
```

### Search helpers

`@rpcbase/db` exposes helpers to run MongoDB search:

```ts
import { buildSearchTextStage, ensureSearchIndex, searchMetaProjection } from "@rpcbase/db"
```

- `buildSearchTextStage(...)` builds a `$search` stage.
- `ensureSearchIndex(...)` creates an index if missing (`listSearchIndexes` + `createSearchIndexes`) and returns `{ created: boolean }`.
- `searchMetaProjection()` adds `score` and `highlights` projection fields.

Sample usage (sample-app) in `sample-app/src/api/search/items/handler.ts`:

```ts
await ensureSearchIndex({ db: Item.db.db, collection: Item.collection.name, name: "item_name_text_v1", definition })
const results = await Item.aggregate([
  buildSearchTextStage({ index: "item_name_text_v1", query, path: "name", highlightPath: "name" }),
  { $limit: 20 },
  { $project: { name: 1, createdAt: 1, ...searchMetaProjection() } },
]).exec()
```

Dans `sample-app`, la config se trouve dans `sample-app/infrastructure` (`compose.yml`, `mongot/config.yml`, `mongot/mongodb-init.js`, `ensure-mongot-*`).