@venizia/ignis-docs 0.0.8-0 → 0.0.8-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  2. package/dist/mcp-server/helpers/docs.helper.js +1 -1
  3. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  4. package/dist/mcp-server/tools/base.tool.d.ts +1 -1
  5. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  6. package/dist/mcp-server/tools/docs/search-documents.tool.js +1 -1
  7. package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
  8. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  9. package/dist/mcp-server/tools/github/list-project-files.tool.js +1 -1
  10. package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
  11. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  12. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  13. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  14. package/package.json +14 -14
  15. package/wiki/extensions/components/authorization/api.md +239 -376
  16. package/wiki/extensions/components/authorization/errors.md +52 -43
  17. package/wiki/extensions/components/authorization/index.md +127 -65
  18. package/wiki/extensions/components/authorization/usage.md +198 -98
  19. package/wiki/extensions/helpers/kafka/consumer.md +6 -5
  20. package/wiki/extensions/helpers/kafka/examples.md +1 -1
  21. package/wiki/extensions/helpers/kafka/index.md +16 -12
  22. package/wiki/extensions/helpers/kafka/producer.md +4 -3
  23. package/wiki/guides/core-concepts/persistent/datasources.md +10 -11
  24. package/wiki/guides/core-concepts/persistent/index.md +6 -6
  25. package/wiki/guides/core-concepts/persistent/models.md +7 -5
  26. package/wiki/guides/core-concepts/persistent/repositories.md +11 -3
  27. package/wiki/guides/core-concepts/persistent/transactions.md +2 -1
  28. package/wiki/guides/core-concepts/rest-controllers.md +2 -2
  29. package/wiki/guides/core-concepts/services.md +0 -1
  30. package/wiki/guides/get-started/5-minute-quickstart.md +11 -10
  31. package/wiki/guides/migrations/scoped-rbac-migration.md +300 -0
  32. package/wiki/guides/tutorials/building-a-crud-api.md +43 -37
  33. package/wiki/guides/tutorials/complete-installation.md +64 -44
  34. package/wiki/guides/tutorials/ecommerce-api.md +21 -12
  35. package/wiki/guides/tutorials/realtime-chat.md +4 -5
  36. package/wiki/references/base/filter-system/default-filter.md +6 -3
  37. package/wiki/references/base/filter-system/fields-order-pagination.md +26 -0
  38. package/wiki/references/base/models.md +6 -3
  39. package/wiki/references/base/repositories/advanced.md +111 -0
@@ -19,15 +19,15 @@ bun init -y
19
19
  ### Production Dependencies
20
20
 
21
21
  ```bash
22
- bun add hono @hono/zod-openapi @scalar/hono-api-reference @venizia/ignis dotenv-flow
22
+ bun add hono @hono/zod-openapi @scalar/hono-api-reference @venizia/ignis @venizia/ignis-helpers
23
23
  ```
24
24
 
25
25
  **What each package does:**
26
26
  - `hono` - High-performance web framework
27
27
  - `@hono/zod-openapi` - OpenAPI schema generation with Zod validation
28
28
  - `@scalar/hono-api-reference` - Interactive API documentation UI
29
- - `@venizia/ignis` - Core Ignis framework
30
- - `dotenv-flow` - Environment variable management
29
+ - `@venizia/ignis` - Core Ignis framework (application, controllers, repositories, DI)
30
+ - `@venizia/ignis-helpers` - Utilities (HTTP constants, logger, environment helpers)
31
31
 
32
32
  ### Development Dependencies
33
33
 
@@ -121,7 +121,7 @@ This setup might seem verbose compared to minimal frameworks. The trade-off: ~50
121
121
  ### Create Project Structure
122
122
 
123
123
  ```bash
124
- mkdir -p src/{common,components,configurations,controllers,datasources,helpers,models/{entities,requests,responses},repositories,services,utilities}
124
+ mkdir -p src/{common,components,configurations,controllers/hello,datasources,helpers,models/{entities,requests,responses},repositories,services,utilities}
125
125
  ```
126
126
 
127
127
  Your structure will look like:
@@ -137,7 +137,10 @@ src/
137
137
  │ └── index.ts # App configuration files
138
138
  ├── controllers/
139
139
  │ ├── index.ts # Export all controllers
140
- │ └── hello.controller.ts
140
+ │ └── hello/
141
+ │ ├── definitions.ts # Route configs, schemas, constants
142
+ │ ├── hello.controller.ts
143
+ │ └── index.ts # Barrel export
141
144
  ├── datasources/
142
145
  │ └── index.ts # Database connections
143
146
  ├── helpers/
@@ -155,6 +158,8 @@ src/
155
158
  └── index.ts # Utility functions
156
159
  ```
157
160
 
161
+ Each controller gets its own folder: `definitions.ts` for route configs and Zod schemas, the controller file itself, and an `index.ts` barrel export. This keeps things organized as controllers grow with custom routes, validations, and transformations.
162
+
158
163
  > **Note:** For this guide, we only use `controllers/`. Other folders will be used in the [CRUD Tutorial](./building-a-crud-api.md) when you add database support.
159
164
 
160
165
  ### Create Application Class
@@ -163,7 +168,7 @@ Create `src/application.ts` - this is where you configure and register all your
163
168
 
164
169
  ```typescript
165
170
  import { BaseApplication, IApplicationConfigs, IApplicationInfo, SwaggerComponent, ValueOrPromise } from '@venizia/ignis';
166
- import { HelloController } from './controllers/hello.controller';
171
+ import { HelloController } from './controllers';
167
172
  import packageJson from '../package.json';
168
173
 
169
174
  // Define application configurations
@@ -255,22 +260,39 @@ export class Application extends BaseApplication {
255
260
 
256
261
  ### Create Controller
257
262
 
258
- Create `src/controllers/hello.controller.ts` - controllers handle HTTP requests and return responses:
263
+ Each controller lives in its own folder with separate files for definitions, logic, and exports.
264
+
265
+ Create `src/controllers/hello/definitions.ts` — route configs and schemas:
259
266
 
260
267
  ```typescript
261
- import {
262
- BaseRestController,
263
- controller,
264
- api,
265
- jsonContent,
266
- } from '@venizia/ignis';
268
+ import { jsonContent } from '@venizia/ignis';
267
269
  import { HTTP } from '@venizia/ignis-helpers';
268
270
  import { z } from '@hono/zod-openapi';
269
- import { Context } from 'hono';
270
271
 
271
- const BASE_PATH = '/hello';
272
+ export const BASE_PATH = '/hello';
273
+
274
+ export const helloRouteConfigs = {
275
+ sayHello: {
276
+ method: HTTP.Methods.GET,
277
+ path: '/',
278
+ responses: {
279
+ [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
280
+ description: 'A simple hello message',
281
+ schema: z.object({ message: z.string() }),
282
+ }),
283
+ },
284
+ },
285
+ } as const;
286
+ ```
287
+
288
+ Create `src/controllers/hello/hello.controller.ts` — the controller class:
289
+
290
+ ```typescript
291
+ import { BaseRestController, controller, api } from '@venizia/ignis';
292
+ import { HTTP } from '@venizia/ignis-helpers';
293
+ import { Context } from 'hono';
294
+ import { BASE_PATH, helloRouteConfigs } from './definitions';
272
295
 
273
- // The @controller decorator registers this class as a controller
274
296
  // All routes in this controller will be under /api/hello (remember path.base: '/api')
275
297
  @controller({ path: BASE_PATH })
276
298
  export class HelloController extends BaseRestController {
@@ -278,47 +300,38 @@ export class HelloController extends BaseRestController {
278
300
  super({ scope: HelloController.name, path: BASE_PATH });
279
301
  }
280
302
 
281
- // Required: Override binding() to register routes or dependencies
282
- override binding() {
283
- // Option 1: Use bindRoute() or defineRoute() for programmatic route registration
284
- // this.bindRoute({ configs: { method: 'get', path: '/programmatic', responses: {...} } }).to({ handler: this.myHandler });
285
-
286
- // Option 2: Use @api decorator on methods (shown below) - recommended
287
- }
303
+ // Override binding() to register custom routes via bindRoute() or defineRoute().
304
+ // For decorator-based routes (@api, @get, @post), this can be empty.
305
+ override binding() {}
288
306
 
289
- // The @api decorator defines a route with explicit method. You can also use @get/@post shorthand decorators.
290
- @api({
291
- configs: {
292
- method: HTTP.Methods.GET,
293
- path: '/',
294
- // This 'responses' config does two things:
295
- // 1. Generates OpenAPI/Swagger documentation automatically
296
- // 2. Validates that your handler returns the correct shape
297
- responses: {
298
- [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
299
- description: 'A simple hello message',
300
- schema: z.object({ message: z.string() }),
301
- }),
302
- },
303
- },
304
- })
307
+ @api({ configs: helloRouteConfigs.sayHello })
305
308
  sayHello(c: Context) {
306
309
  return c.json({ message: 'Hello, World!' }, HTTP.ResultCodes.RS_2.Ok);
307
310
  }
308
-
309
- // For authenticated endpoints, add 'authenticate':
310
- // @api({ configs: { method: HTTP.Methods.GET, path: '/secure', authenticate: { strategies: [Authentication.STRATEGY_JWT] } } })
311
311
  }
312
312
  ```
313
313
 
314
+ Create `src/controllers/hello/index.ts` — barrel export:
315
+
316
+ ```typescript
317
+ export * from './hello.controller';
318
+ ```
319
+
320
+ Create `src/controllers/index.ts` — export all controllers:
321
+
322
+ ```typescript
323
+ export * from './hello';
324
+ ```
325
+
314
326
  **Controller Patterns:**
315
327
 
316
328
  | Pattern | Description |
317
329
  |---------|-------------|
318
- | `@controller` | Registers the class as a controller with a base path. Supports optional `transport` field (`'rest'` default, `'grpc'`) |
330
+ | `definitions.ts` | Route configs, Zod schemas, and constants keeps controller file clean |
331
+ | `@controller` | Registers the class as a controller with a base path |
319
332
  | `@api` | Defines a route with `method` specified in configs |
320
333
  | `@get`, `@post`, etc. | Shorthand decorators that auto-set the HTTP method (recommended) |
321
- | `binding()` | Required override use `bindRoute()` or `defineRoute()` for programmatic routes |
334
+ | `binding()` | Override for programmatic routes via `bindRoute()` / `defineRoute()`. Empty for decorator-based routes |
322
335
  | Zod schemas | Provide automatic validation and OpenAPI docs |
323
336
 
324
337
  > **Deep Dive:** See [Controllers Reference](../core-concepts/rest-controllers.md) for advanced routing patterns and validation.
@@ -341,6 +354,10 @@ const main = async () => {
341
354
 
342
355
  const applicationName = process.env.APP_ENV_APPLICATION_NAME?.toUpperCase() ?? 'My-App';
343
356
  logger.info('[main] Getting ready to start up %s Application...', applicationName);
357
+
358
+ // start() runs the full lifecycle automatically:
359
+ // staticConfigure → preConfigure → registerDataSources → registerComponents
360
+ // → registerControllers → postConfigure → setupMiddlewares → HTTP server
344
361
  await application.start();
345
362
  return application;
346
363
  };
@@ -348,6 +365,9 @@ const main = async () => {
348
365
  export default main();
349
366
  ```
350
367
 
368
+ > [!TIP]
369
+ > `start()` runs the full lifecycle internally. You only need the explicit `init()` → `boot()` → `start()` sequence when using **auto-discovery** (glob-based booters) to discover controllers/repositories from the filesystem. For manual registration (as shown here), `start()` alone is sufficient.
370
+
351
371
  ## 5. Run Your Application
352
372
 
353
373
  Add common application scripts to your `package.json`:
@@ -28,7 +28,7 @@ cd ecommerce-api
28
28
  bun init -y
29
29
 
30
30
  # Install dependencies
31
- bun add hono @hono/zod-openapi @venizia/ignis dotenv-flow
31
+ bun add hono @hono/zod-openapi @venizia/ignis @venizia/ignis-helpers
32
32
  bun add drizzle-orm drizzle-zod pg stripe
33
33
  bun add -d typescript @types/bun @venizia/dev-configs drizzle-kit @types/pg
34
34
  ```
@@ -57,9 +57,19 @@ ecommerce-api/
57
57
  │ │ ├── order.service.ts
58
58
  │ │ └── payment.service.ts
59
59
  │ ├── controllers/
60
- │ │ ├── product.controller.ts
61
- │ │ ├── cart.controller.ts
62
- │ │ └── order.controller.ts
60
+ │ │ ├── product/
61
+ │ │ ├── definitions.ts
62
+ │ │ │ ├── product.controller.ts
63
+ │ │ │ └── index.ts
64
+ │ │ ├── cart/
65
+ │ │ │ ├── definitions.ts
66
+ │ │ │ ├── cart.controller.ts
67
+ │ │ │ └── index.ts
68
+ │ │ ├── order/
69
+ │ │ │ ├── definitions.ts
70
+ │ │ │ ├── order.controller.ts
71
+ │ │ │ └── index.ts
72
+ │ │ └── index.ts
63
73
  │ └── datasources/
64
74
  │ └── postgres.datasource.ts
65
75
  └── package.json
@@ -314,7 +324,6 @@ export * from './order.model';
314
324
  import {
315
325
  BaseDataSource,
316
326
  datasource,
317
- TNodePostgresConnector,
318
327
  ValueOrPromise,
319
328
  } from '@venizia/ignis';
320
329
  import { drizzle } from 'drizzle-orm/node-postgres';
@@ -329,7 +338,7 @@ interface IDSConfigs {
329
338
  }
330
339
 
331
340
  @datasource({ driver: 'node-postgres' })
332
- export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
341
+ export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
333
342
  constructor() {
334
343
  super({
335
344
  name: PostgresDataSource.name,
@@ -915,7 +924,7 @@ export class PaymentService extends BaseService {
915
924
  ### Product Controller
916
925
 
917
926
  ```typescript
918
- // src/controllers/product.controller.ts
927
+ // src/controllers/product/product.controller.ts
919
928
  import { z } from '@hono/zod-openapi';
920
929
  import {
921
930
  BaseRestController,
@@ -1003,7 +1012,7 @@ export class ProductController extends BaseRestController {
1003
1012
  ### Cart Controller
1004
1013
 
1005
1014
  ```typescript
1006
- // src/controllers/cart.controller.ts
1015
+ // src/controllers/cart/cart.controller.ts
1007
1016
  import { z } from '@hono/zod-openapi';
1008
1017
  import {
1009
1018
  BaseRestController,
@@ -1142,7 +1151,7 @@ export class CartController extends BaseRestController {
1142
1151
  ### Order Controller
1143
1152
 
1144
1153
  ```typescript
1145
- // src/controllers/order.controller.ts
1154
+ // src/controllers/order/order.controller.ts
1146
1155
  import { z } from '@hono/zod-openapi';
1147
1156
  import {
1148
1157
  BaseRestController,
@@ -1265,9 +1274,9 @@ export class OrderController extends BaseRestController {
1265
1274
  import { BaseApplication, IApplicationInfo } from '@venizia/ignis';
1266
1275
  import { HealthCheckComponent, SwaggerComponent } from '@venizia/ignis';
1267
1276
 
1268
- import { ProductController } from './controllers/product.controller';
1269
- import { CartController } from './controllers/cart.controller';
1270
- import { OrderController } from './controllers/order.controller';
1277
+ import { ProductController } from './controllers/product';
1278
+ import { CartController } from './controllers/cart';
1279
+ import { OrderController } from './controllers/order';
1271
1280
 
1272
1281
  import { ProductService } from './services/product.service';
1273
1282
  import { CartService } from './services/cart.service';
@@ -25,7 +25,7 @@ cd chat-api
25
25
  bun init -y
26
26
 
27
27
  # Install dependencies
28
- bun add hono @hono/zod-openapi @venizia/ignis dotenv-flow
28
+ bun add hono @hono/zod-openapi @venizia/ignis @venizia/ignis-helpers
29
29
  bun add drizzle-orm drizzle-zod pg
30
30
  bun add -d typescript @types/bun @venizia/dev-configs drizzle-kit @types/pg
31
31
 
@@ -241,7 +241,6 @@ export * from './message.model';
241
241
  import {
242
242
  BaseDataSource,
243
243
  datasource,
244
- TNodePostgresConnector,
245
244
  ValueOrPromise,
246
245
  } from '@venizia/ignis';
247
246
  import { drizzle } from 'drizzle-orm/node-postgres';
@@ -256,7 +255,7 @@ interface IDSConfigs {
256
255
  }
257
256
 
258
257
  @datasource({ driver: 'node-postgres' })
259
- export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
258
+ export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
260
259
  constructor() {
261
260
  super({
262
261
  name: PostgresDataSource.name,
@@ -953,7 +952,7 @@ import {
953
952
  SocketIOServerHelper,
954
953
  } from '@venizia/ignis-helpers';
955
954
  import { ChatService } from './services/chat.service';
956
- import { ChatController } from './controllers/chat.controller';
955
+ import { ChatController } from './controllers/chat';
957
956
  import { UserRepository } from './repositories/user.repository';
958
957
  import { RoomRepository, RoomMemberRepository } from './repositories/room.repository';
959
958
  import { MessageRepository, DirectMessageRepository } from './repositories/message.repository';
@@ -1124,7 +1123,7 @@ Client connects → 'authenticate' event → authenticateFn() → 'authenticated
1124
1123
  ## 7. REST API for Chat History
1125
1124
 
1126
1125
  ```typescript
1127
- // src/controllers/chat.controller.ts
1126
+ // src/controllers/chat/chat.controller.ts
1128
1127
  import { z } from '@hono/zod-openapi';
1129
1128
  import {
1130
1129
  BaseRestController,
@@ -299,13 +299,13 @@ export class Subscription extends BaseEntity<typeof Subscription.schema> {}
299
299
 
300
300
  ### Query Limit Protection
301
301
 
302
+ Use the dedicated `settings.defaultLimit` to raise (or lower) the per-model default page size. Prefer it over putting `limit` inside `defaultFilter`:
303
+
302
304
  ```typescript
303
305
  @model({
304
306
  type: 'entity',
305
307
  settings: {
306
- defaultFilter: {
307
- limit: 1000, // Prevent unbounded queries
308
- },
308
+ defaultLimit: 1000, // Per-model default when a query omits `limit`
309
309
  },
310
310
  })
311
311
  export class LogEntry extends BaseEntity<typeof LogEntry.schema> {}
@@ -315,6 +315,9 @@ await logRepo.find({ filter: {} }); // LIMIT 1000
315
315
  await logRepo.find({ filter: { limit: 50 } }); // LIMIT 50
316
316
  ```
317
317
 
318
+ > [!TIP]
319
+ > `defaultLimit` is independent of `defaultFilter`: bypassing the default filter via `shouldSkipDefaultFilter` does **not** drop the limit. See [Pagination → Default Limit](/references/base/filter-system/fields-order-pagination#default-limit).
320
+
318
321
 
319
322
  ## Relation Include Default Filters
320
323
 
@@ -132,6 +132,32 @@ await repo.find({
132
132
  > [!TIP]
133
133
  > Always use `limit` for public-facing endpoints to prevent memory exhaustion. The default limit is 10 if not specified.
134
134
 
135
+ ### Default Limit
136
+
137
+ When a query omits `limit`, the repository resolves one with this precedence:
138
+
139
+ ```
140
+ query.limit ?? model settings.defaultLimit ?? DEFAULT_LIMIT (10)
141
+ ```
142
+
143
+ - **`query.limit`** — an explicit `limit` in the filter always wins.
144
+ - **`settings.defaultLimit`** — a per-model default set on the `@model` decorator. Must be a positive integer (validated at decoration time). Applies to top-level `find()` and to every to-many relation (using the related model's own `defaultLimit`).
145
+ - **`DEFAULT_LIMIT`** — the global fallback, `10`.
146
+
147
+ ```typescript
148
+ @model({
149
+ type: 'entity',
150
+ settings: { defaultLimit: 200 }, // Small lookup table — default to 200 rows
151
+ })
152
+ export class Country extends BaseEntity<typeof Country.schema> {}
153
+
154
+ await countryRepo.find({ filter: {} }); // LIMIT 200
155
+ await countryRepo.find({ filter: { limit: 10 } }); // LIMIT 10 (explicit wins)
156
+ ```
157
+
158
+ > [!NOTE]
159
+ > `defaultLimit` is independent of `defaultFilter`: passing `shouldSkipDefaultFilter` to bypass the default `where` clause does **not** drop the default limit. There is no "unbounded" sentinel — to fetch more rows, pass an explicit `limit`.
160
+
135
161
  ### Pagination Helper
136
162
 
137
163
  ```typescript
@@ -51,6 +51,7 @@ The `@model` decorator marks a class as a database entity and configures its beh
51
51
  settings?: {
52
52
  hiddenProperties?: string[], // Properties to exclude from query results
53
53
  defaultFilter?: TFilter, // Filter applied to all repository queries
54
+ defaultLimit?: number, // Default row limit when a query omits `limit`
54
55
  authorize?: { // Authorization settings
55
56
  principal: string, // Authorization subject name
56
57
  [extra: string | symbol]: any, // Extensible metadata
@@ -66,15 +67,17 @@ The `@model` decorator marks a class as a database entity and configures its beh
66
67
  | `skipMigrate` | `boolean` | Skip this model during schema migrations |
67
68
  | `settings.hiddenProperties` | `string[]` | Array of property names to exclude from all repository query results |
68
69
  | `settings.defaultFilter` | `TFilter` | Filter automatically applied to all repository queries (see [Default Filter](/references/base/filter-system/default-filter)) |
70
+ | `settings.defaultLimit` | `number` | Default row limit applied when a query omits `limit`. Must be a positive integer (validated at decoration time). Falls back to the global `DEFAULT_LIMIT` (10). See [Pagination](/references/base/filter-system/fields-order-pagination#default-limit) |
69
71
  | `settings.authorize` | `IModelAuthorizeSettings` | Authorization settings — declares the model's authorization principal (see [Authorization](/extensions/components/authorization/usage#model-based-resource-references)) |
70
72
  | `settings.authorize.principal` | `string` | The authorization subject name for this model. Auto-populates `AUTHORIZATION_SUBJECT` static property |
71
73
 
72
74
  #### `@model` Behavior
73
75
 
74
76
  When the `@model` decorator is applied:
75
- 1. If `settings.authorize.principal` is provided and `AUTHORIZATION_SUBJECT` is not already defined on the class, it auto-populates `AUTHORIZATION_SUBJECT` with the principal value
76
- 2. The model is registered in the `MetadataRegistry` model registry, keyed by table name (resolved as: `metadata.tableName` > `static TABLE_NAME` > class name)
77
- 3. The static `relations` property is stored as a resolver (not immediately resolved) to avoid circular dependency issues between models
77
+ 1. If `settings.defaultLimit` is provided, it is validated to be a positive integer otherwise the decorator throws at decoration (boot) time
78
+ 2. If `settings.authorize.principal` is provided and `AUTHORIZATION_SUBJECT` is not already defined on the class, it auto-populates `AUTHORIZATION_SUBJECT` with the principal value
79
+ 3. The model is registered in the `MetadataRegistry` model registry, keyed by table name (resolved as: `metadata.tableName` > `static TABLE_NAME` > class name)
80
+ 4. The static `relations` property is stored as a resolver (not immediately resolved) to avoid circular dependency issues between models
78
81
 
79
82
  ### Hidden Properties
80
83
 
@@ -101,6 +101,114 @@ async function transferFunds(fromId: string, toId: string, amount: number) {
101
101
  ```
102
102
 
103
103
 
104
+ ## Row-Level Locking
105
+
106
+ Acquire pessimistic locks on selected rows within a transaction using PostgreSQL's `SELECT ... FOR UPDATE/SHARE` syntax.
107
+
108
+ ### Basic Usage
109
+
110
+ Pass `lock` in options alongside a `transaction`:
111
+
112
+ ```typescript
113
+ const tx = await repo.beginTransaction();
114
+
115
+ try {
116
+ // Lock the row — other transactions will wait
117
+ const item = await repo.findOne({
118
+ filter: { where: { id: '123' } },
119
+ options: {
120
+ transaction: tx,
121
+ lock: { strength: 'update' },
122
+ },
123
+ });
124
+
125
+ // Safe to modify — no concurrent changes possible
126
+ await repo.updateById({
127
+ id: '123',
128
+ data: { quantity: item.quantity - 1 },
129
+ options: { transaction: tx },
130
+ });
131
+
132
+ await tx.commit();
133
+ } catch (error) {
134
+ await tx.rollback();
135
+ throw error;
136
+ }
137
+ ```
138
+
139
+ ### Lock Strengths
140
+
141
+ Use the `LockStrengths` constant class or string literals:
142
+
143
+ ```typescript
144
+ import { LockStrengths } from '@venizia/ignis';
145
+
146
+ // Using constant
147
+ lock: { strength: LockStrengths.UPDATE }
148
+
149
+ // Using string literal
150
+ lock: { strength: 'update' }
151
+ ```
152
+
153
+ | Strength | SQL | Use Case |
154
+ |----------|-----|----------|
155
+ | `update` | `FOR UPDATE` | Exclusive lock for writes |
156
+ | `no key update` | `FOR NO KEY UPDATE` | Exclusive lock, allows concurrent `FOR KEY SHARE` |
157
+ | `share` | `FOR SHARE` | Shared read lock, prevents writes |
158
+ | `key share` | `FOR KEY SHARE` | Weakest lock, only prevents key changes |
159
+
160
+ ### Wait Behavior
161
+
162
+ Control what happens when rows are already locked:
163
+
164
+ ```typescript
165
+ // Skip locked rows (queue-style worker pattern)
166
+ const items = await repo.find({
167
+ filter: { where: { status: 'pending' }, limit: 10 },
168
+ options: {
169
+ transaction: tx,
170
+ lock: { strength: 'update', config: { skipLocked: true } },
171
+ },
172
+ });
173
+
174
+ // Fail immediately instead of waiting
175
+ const item = await repo.findOne({
176
+ filter: { where: { id: '123' } },
177
+ options: {
178
+ transaction: tx,
179
+ lock: { strength: 'update', config: { noWait: true } },
180
+ },
181
+ });
182
+ ```
183
+
184
+ | Config | SQL | Behavior |
185
+ |--------|-----|----------|
186
+ | *(none)* | `FOR UPDATE` | Wait until lock is released |
187
+ | `{ noWait: true }` | `FOR UPDATE NOWAIT` | Throw error immediately if locked |
188
+ | `{ skipLocked: true }` | `FOR UPDATE SKIP LOCKED` | Silently skip locked rows |
189
+
190
+ ### Constraints
191
+
192
+ > [!WARNING]
193
+ > Row-level locking requires a **transaction** and is **incompatible with `include`/`fields`** in the filter (these use the Drizzle Query API which does not support `.for()`).
194
+
195
+ ```typescript
196
+ // Error — no transaction
197
+ await repo.findOne({
198
+ filter: { where: { id: '123' } },
199
+ options: { lock: { strength: 'update' } },
200
+ });
201
+
202
+ // Error — include uses Query API
203
+ await repo.findOne({
204
+ filter: { where: { id: '123' }, include: [{ relation: 'posts' }] },
205
+ options: { transaction: tx, lock: { strength: 'update' } },
206
+ });
207
+ ```
208
+
209
+ **Supported methods:** `find`, `findOne`, `findById`
210
+
211
+
104
212
  ## Hidden Properties
105
213
 
106
214
  Automatically exclude sensitive fields from query results.
@@ -599,6 +707,7 @@ All repository operations accept an `options` parameter with these fields:
599
707
  | `transaction` | `ITransaction` | - | Transaction context for the operation |
600
708
  | `log` | `{ use: boolean; level?: TLogLevel }` | - | Enable operation logging |
601
709
  | `shouldSkipDefaultFilter` | `boolean` | `false` | Bypass the default filter from model settings |
710
+ | `lock` | `TLockOptions` | - | Row-level locking (requires transaction, Core API only) |
602
711
 
603
712
  Write operations additionally support:
604
713
 
@@ -618,6 +727,8 @@ Write operations additionally support:
618
727
  | Commit | `await tx.commit()` |
619
728
  | Rollback | `await tx.rollback()` |
620
729
  | Bypass default filter | `options: { shouldSkipDefaultFilter: true }` |
730
+ | Lock rows for update | `options: { transaction: tx, lock: { strength: 'update' } }` |
731
+ | Lock + skip locked | `options: { transaction: tx, lock: { strength: 'update', config: { skipLocked: true } } }` |
621
732
  | Enable logging | `options: { log: { use: true, level: 'debug' } }` |
622
733
  | Force delete all | `options: { force: true }` |
623
734
  | Skip returning data | `options: { shouldReturn: false }` |