Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
Changelog

## [4.13.0](https://github.com/MikeDev75015/mongodb-dynamic-api/compare/v4.12.0...v4.13.0) (2026-05-26)

### route-config

* **route-config:** replace flat DynamicApiRouteConfig with discriminated union ([6046c61](https://github.com/MikeDev75015/mongodb-dynamic-api/commit/6046c6159eb43e68d9efec5bc5890fd9f4c1aadb))

## [4.12.0](https://github.com/MikeDev75015/mongodb-dynamic-api/compare/v4.11.0...v4.12.0) (2026-05-26)

### auth
Expand Down
32 changes: 20 additions & 12 deletions README/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ const onOrderCreatedAlt: AfterSaveCallback<Order> = async (order, methods, user)

The `beforeSaveCallback` property is executed **before the database write**. Its purpose is to transform, enrich, or validate the data before it is persisted. The return value **replaces** the data that will be saved (except for delete callbacks which return `void`).

> **✅ No cast needed — discriminated union:** Since `DynamicApiRouteConfig<E>` is a discriminated union, TypeScript narrows `beforeSaveCallback` to the exact context type for each `type` discriminant. You write `ctx.toCreate`, `ctx.update`, `ctx.replacement`, etc. with full autocompletion and no `as` cast.

### Four callback signatures

The signature varies depending on whether the route operates on a single document, multiple documents, or a delete operation.
Expand Down Expand Up @@ -451,19 +453,25 @@ DynamicApiModule.forFeature({

### Signature-to-route compatibility

| Route Type | Callback signature | Context type | `BodyDTO` supported | `entity` / `entities` | Returns |
`DynamicApiRouteConfig<E>` is a **discriminated union** — TypeScript automatically narrows `beforeSaveCallback` to the exact callback type for each `type` discriminant, eliminating all manual casts in application code.

| Route Type | Config type | Callback signature | Context type | `BodyDTO` supported | Returns |
|---|---|---|---|---|---|
| `CreateOne` | `BeforeSaveCallback` | `BeforeSaveCreateContext<E, BodyDTO>` | ✅ | `undefined` | `Partial<E>` |
| `CreateMany` | `BeforeSaveListCallback` | `BeforeSaveCreateManyContext<E, BodyDTO>` | ✅ | `undefined` | `Partial<E>[]` |
| `UpdateOne` | `BeforeSaveCallback` | `BeforeSaveUpdateContext<E, BodyDTO>` | ✅ | Existing entity | `Partial<E>` |
| `UpdateMany` | `BeforeSaveListCallback` | `BeforeSaveUpdateManyContext<E, BodyDTO>` | ✅ | Existing entities | `Partial<E>[]` |
| `ReplaceOne` | `BeforeSaveCallback` | `BeforeSaveReplaceContext<E, BodyDTO>` | ✅ | Existing entity | `Partial<E>` |
| `DuplicateOne` | `BeforeSaveCallback` | `BeforeSaveDuplicateContext<E, BodyDTO>` | ✅ | Existing entity | `Partial<E>` |
| `DuplicateMany` | `BeforeSaveListCallback` | `BeforeSaveDuplicateManyContext<E, BodyDTO>` | ✅ | Existing entities | `Partial<E>[]` |
| `DeleteOne` | `BeforeSaveDeleteCallback` | `BeforeSaveDeleteContext` | — | Entity to delete | `void` |
| `DeleteMany` | `BeforeSaveDeleteManyCallback` | `BeforeSaveDeleteManyContext` | — | Entities to delete | `void` |

> **⚠️ Note:** `GetOne` and `GetMany` do **not** support `beforeSaveCallback` — they only support the `callback` (after save) property.
| `CreateOne` | `CreateOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveCreateContext<E>>` | `BeforeSaveCreateContext<E, BodyDTO>` | ✅ | `Partial<E>` |
| `CreateMany` | `CreateManyRouteConfig<E>` | `BeforeSaveListCallback<E, BeforeSaveCreateManyContext<E>>` | `BeforeSaveCreateManyContext<E, BodyDTO>` | ✅ | `Partial<E>[]` |
| `UpdateOne` | `UpdateOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveUpdateContext<E>>` | `BeforeSaveUpdateContext<E, BodyDTO>` | ✅ | `Partial<E>` |
| `UpdateMany` | `UpdateManyRouteConfig<E>` | `BeforeSaveListCallback<E, BeforeSaveUpdateManyContext<E>>` | `BeforeSaveUpdateManyContext<E, BodyDTO>` | ✅ | `Partial<E>[]` |
| `ReplaceOne` | `ReplaceOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveReplaceContext<E>>` | `BeforeSaveReplaceContext<E, BodyDTO>` | ✅ | `Partial<E>` |
| `DuplicateOne` | `DuplicateOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveDuplicateContext<E>>` | `BeforeSaveDuplicateContext<E, BodyDTO>` | ✅ | `Partial<E>` |
| `DuplicateMany` | `DuplicateManyRouteConfig<E>` | `BeforeSaveListCallback<E, BeforeSaveDuplicateManyContext<E>>` | `BeforeSaveDuplicateManyContext<E, BodyDTO>` | ✅ | `Partial<E>[]` |
| `DeleteOne` | `DeleteOneRouteConfig<E>` | `BeforeSaveDeleteCallback<E, BeforeSaveDeleteContext>` | `BeforeSaveDeleteContext` | — | `void` |
| `DeleteMany` | `DeleteManyRouteConfig<E>` | `BeforeSaveDeleteManyCallback<E, BeforeSaveDeleteManyContext>` | `BeforeSaveDeleteManyContext` | — | `void` |

> **⚠️ Note:** `GetOne`, `GetMany`, `Aggregate` and `Custom` routes do **not** have a `beforeSaveCallback` field — they only support the `callback` (after-save) property.

> **⚠️ Breaking change:** `beforeDeleteCallback` and `cascade` are now exclusive to `DeleteOneRouteConfig` and `DeleteManyRouteConfig`. Placing them on any other route type is a compile-time error.

> **`AnyBeforeSaveCallback`** is `@deprecated` — it is no longer needed in application code when using the discriminated union. It remains exported for backward compatibility with generic third-party helpers and will be removed in v5.

---

Expand Down
40 changes: 25 additions & 15 deletions README/route-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,20 +460,31 @@ Callbacks let you hook into the lifecycle of a service operation, either **befor

### beforeSaveCallback

Four signatures depending on the route type:

| Callback type | Used by | Context type | Returns |
|---|---|---|---|
| `BeforeSaveCallback<E, Context, User>` | `CreateOne`, `UpdateOne`, `ReplaceOne`, `DuplicateOne` | `BeforeSave*Context<E, BodyDTO>` | `Partial<E>` |
| `BeforeSaveListCallback<E, Context, User>` | `CreateMany`, `UpdateMany`, `DuplicateMany` | `BeforeSave*Context<E, BodyDTO>` | `Partial<E>[]` |
| `BeforeSaveDeleteCallback<E, Context, User>` | `DeleteOne` | `BeforeSaveDeleteContext` | `void` |
| `BeforeSaveDeleteManyCallback<E, Context, User>` | `DeleteMany` | `BeforeSaveDeleteManyContext` | `void` |
`DynamicApiRouteConfig` is a **discriminated union** — TypeScript narrows `beforeSaveCallback` to the exact context type for each `type` discriminant. No cast is ever needed in application code.

| Route type | Config type | Callback type | Context type | Returns |
|---|---|---|---|---|
| `CreateOne` | `CreateOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveCreateContext<E>>` | `BeforeSaveCreateContext<E, BodyDTO>` | `Partial<E>` |
| `CreateMany` | `CreateManyRouteConfig<E>` | `BeforeSaveListCallback<E, BeforeSaveCreateManyContext<E>>` | `BeforeSaveCreateManyContext<E, BodyDTO>` | `Partial<E>[]` |
| `UpdateOne` | `UpdateOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveUpdateContext<E>>` | `BeforeSaveUpdateContext<E, BodyDTO>` | `Partial<E>` |
| `UpdateMany` | `UpdateManyRouteConfig<E>` | `BeforeSaveListCallback<E, BeforeSaveUpdateManyContext<E>>` | `BeforeSaveUpdateManyContext<E, BodyDTO>` | `Partial<E>[]` |
| `ReplaceOne` | `ReplaceOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveReplaceContext<E>>` | `BeforeSaveReplaceContext<E, BodyDTO>` | `Partial<E>` |
| `DuplicateOne` | `DuplicateOneRouteConfig<E>` | `BeforeSaveCallback<E, BeforeSaveDuplicateContext<E>>` | `BeforeSaveDuplicateContext<E, BodyDTO>` | `Partial<E>` |
| `DuplicateMany` | `DuplicateManyRouteConfig<E>` | `BeforeSaveListCallback<E, BeforeSaveDuplicateManyContext<E>>` | `BeforeSaveDuplicateManyContext<E, BodyDTO>` | `Partial<E>[]` |
| `DeleteOne` | `DeleteOneRouteConfig<E>` | `BeforeSaveDeleteCallback<E, BeforeSaveDeleteContext>` | `BeforeSaveDeleteContext` | `void` |
| `DeleteMany` | `DeleteManyRouteConfig<E>` | `BeforeSaveDeleteManyCallback<E, BeforeSaveDeleteManyContext>` | `BeforeSaveDeleteManyContext` | `void` |
| `GetOne` | `GetOneRouteConfig<E>` | — *(no beforeSaveCallback)* | — | — |
| `GetMany` | `GetManyRouteConfig<E>` | — *(no beforeSaveCallback)* | — | — |
| `Aggregate` | `AggregateRouteConfig<E>` | — *(no beforeSaveCallback)* | — | — |
| `Custom` | `CustomOperationRouteConfig<E>` | — *(no beforeSaveCallback)* | — | — |

> **`beforeDeleteCallback` and `cascade`** are only available on `DeleteOneRouteConfig` and `DeleteManyRouteConfig`.

Each route provides a typed context (`BeforeSaveCreateContext`, `BeforeSaveUpdateContext`, etc.) and the authenticated `user` as the last parameter. The `User` generic defaults to `unknown` — pass your user entity type for full type safety (see [Callbacks guide](https://github.com/MikeDev75015/mongodb-dynamic-api/blob/main/README/callbacks.md#typing-the-user-parameter)).

Context types accept an optional **`BodyDTO` generic** (defaults to `Entity`). Pass your custom body DTO class when using `dTOs.body` to get full type safety on the body fields (see [Custom Body DTO](https://github.com/MikeDev75015/mongodb-dynamic-api/blob/main/README/callbacks.md#custom-body-dto--bodydto-generic)).

**Example — Hash a password before saving (typed context):**
**Example — Hash a password before saving (typed context, no cast):**

```typescript
import * as bcrypt from 'bcrypt';
Expand All @@ -485,14 +496,13 @@ class CreateUserDto {
displayName?: string;
}

// ✅ BodyDTO generic — ctx.toCreate is Partial<CreateUserDto>, no cast needed
const beforeCreate: BeforeSaveCallback<User, BeforeSaveCreateContext<User, CreateUserDto>> =
// ✅ ctx.toCreate is Partial<User> — TS narrows automatically via discriminant type: 'CreateOne'
const beforeCreate: BeforeSaveCallback<User, BeforeSaveCreateContext<User>> =
async (_entity, ctx, _methods, _user) => {
const { toCreate } = ctx;
if (toCreate.password) {
toCreate.password = await bcrypt.hash(toCreate.password, 10);
if (ctx.toCreate.password) {
ctx.toCreate.password = await bcrypt.hash(ctx.toCreate.password, 10);
}
return toCreate;
return ctx.toCreate;
};

DynamicApiModule.forFeature({
Expand Down
Loading