@venizia/ignis-docs 0.0.1-7 → 0.0.1-9

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 (27) hide show
  1. package/package.json +12 -12
  2. package/wiki/changelogs/2025-12-17-refactor.md +22 -0
  3. package/wiki/changelogs/2025-12-18-performance-optimizations.md +192 -0
  4. package/wiki/changelogs/2025-12-18-repository-validation-security.md +445 -0
  5. package/wiki/changelogs/index.md +22 -0
  6. package/wiki/changelogs/v0.0.1-7-initial-architecture.md +137 -0
  7. package/wiki/changelogs/v0.0.1-8-model-repo-datasource-refactor.md +278 -0
  8. package/wiki/get-started/5-minute-quickstart.md +1 -1
  9. package/wiki/get-started/best-practices/api-usage-examples.md +12 -8
  10. package/wiki/get-started/best-practices/common-pitfalls.md +2 -2
  11. package/wiki/get-started/best-practices/data-modeling.md +14 -20
  12. package/wiki/get-started/building-a-crud-api.md +60 -75
  13. package/wiki/get-started/core-concepts/controllers.md +14 -14
  14. package/wiki/get-started/core-concepts/persistent.md +110 -130
  15. package/wiki/get-started/quickstart.md +1 -1
  16. package/wiki/references/base/controllers.md +40 -16
  17. package/wiki/references/base/datasources.md +195 -33
  18. package/wiki/references/base/dependency-injection.md +5 -5
  19. package/wiki/references/base/models.md +398 -28
  20. package/wiki/references/base/repositories.md +475 -22
  21. package/wiki/references/components/authentication.md +224 -7
  22. package/wiki/references/components/health-check.md +1 -1
  23. package/wiki/references/components/swagger.md +1 -1
  24. package/wiki/references/helpers/inversion.md +8 -3
  25. package/wiki/references/src-details/core.md +6 -5
  26. package/wiki/references/src-details/inversion.md +4 -4
  27. package/wiki/references/utilities/request.md +16 -7
@@ -21,11 +21,16 @@ Base class for all repositories - sets up fundamental properties and dependencie
21
21
 
22
22
  ### Key Properties
23
23
 
24
- - `entity` (`BaseEntity`): An instance of the model class associated with this repository. It provides access to the Drizzle schema.
25
- - `dataSource` (`IDataSource`): The datasource instance injected into the repository, which holds the database connection.
24
+ - `entity` (`BaseEntity`): An instance of the model class associated with this repository. It provides access to the Drizzle schema. Auto-resolved from `@repository` metadata or passed in constructor.
25
+ - `dataSource` (`IDataSource`): The datasource instance injected into the repository, which holds the database connection. Auto-injected from `@repository` decorator or passed in constructor.
26
26
  - `connector`: A getter that provides direct access to the Drizzle ORM instance from the datasource.
27
27
  - `filterBuilder` (`DrizzleFilterBuilder`): An instance of the filter builder responsible for converting `Ignis`'s filter objects into Drizzle-compatible query options.
28
- - `relations` (`{ [relationName: string]: TRelationConfig }`): A map of relation configurations defined for the entity.
28
+ - `operationScope` (`TRepositoryOperationScope`): Defines whether the repository is read-only or read-write.
29
+ - `defaultLimit` (`number`): Default limit for queries (default: 10).
30
+
31
+ ### Key Methods
32
+
33
+ - `getEntityRelations()`: Returns a map of relation configurations from the entity's static `relations` property.
29
34
 
30
35
  ### Abstract Methods
31
36
 
@@ -62,7 +67,7 @@ The `ReadableRepository` provides a **read-only** implementation of the reposito
62
67
 
63
68
  ## `PersistableRepository`
64
69
 
65
- The `PersistableRepository` extends `ReadableRepository` and adds **write operations**. It provides the core logic for creating, updating, and deleting records.
70
+ The `PersistableRepository` extends `ReadableRepository` and adds **write operations**. It provides the core logic for creating, updating, and deleting records with built-in safety mechanisms.
66
71
 
67
72
  - **File:** `packages/core/src/base/repositories/core/persistable.ts`
68
73
 
@@ -72,8 +77,64 @@ The `PersistableRepository` extends `ReadableRepository` and adds **write operat
72
77
  - `createAll(opts)`
73
78
  - `updateById(opts)`
74
79
  - `updateAll(opts)`
80
+ - `updateBy(opts)` - Alias for `updateAll`
75
81
  - `deleteById(opts)`
76
82
  - `deleteAll(opts)`
83
+ - `deleteBy(opts)` - Alias for `deleteAll`
84
+
85
+ ### Safety Features
86
+
87
+ #### Empty Where Clause Protection
88
+
89
+ The `PersistableRepository` includes safety mechanisms to prevent accidental mass updates or deletions:
90
+
91
+ **Update Operations (`updateAll`):**
92
+ ```typescript
93
+ // ❌ Throws error: Empty where condition without force flag
94
+ await repository.updateAll({
95
+ data: { status: 'inactive' },
96
+ where: {}, // Empty condition
97
+ });
98
+
99
+ // ✅ Warning logged: Explicitly allow mass update with force flag
100
+ await repository.updateAll({
101
+ data: { status: 'inactive' },
102
+ where: {},
103
+ options: { force: true }, // Force flag allows empty where
104
+ });
105
+ ```
106
+
107
+ **Delete Operations (`deleteAll`):**
108
+ ```typescript
109
+ // ❌ Throws error: Empty where condition without force flag
110
+ await repository.deleteAll({
111
+ where: {}, // Empty condition
112
+ });
113
+
114
+ // ✅ Warning logged: Explicitly allow mass delete with force flag
115
+ await repository.deleteAll({
116
+ where: {},
117
+ options: { force: true }, // Force flag allows empty where
118
+ });
119
+ ```
120
+
121
+ #### Behavior Summary
122
+
123
+ | Scenario | `force: false` (default) | `force: true` |
124
+ |----------|-------------------------|---------------|
125
+ | Empty `where` clause | ❌ Throws error | ✅ Logs warning and proceeds |
126
+ | Valid `where` clause | ✅ Executes normally | ✅ Executes normally |
127
+
128
+ **Warning Messages:**
129
+
130
+ When performing operations with empty `where` conditions and `force: true`, the repository logs a warning:
131
+
132
+ ```
133
+ [_update] Entity: MyEntity | Performing update with empty condition | data: {...} | condition: {}
134
+ [_delete] Entity: MyEntity | Performing delete with empty condition | condition: {}
135
+ ```
136
+
137
+ This helps track potentially dangerous operations in your logs.
77
138
 
78
139
  You will typically not use this class directly, but rather the `DefaultCRUDRepository`.
79
140
 
@@ -83,36 +144,428 @@ This is the primary class you should extend for repositories that require full *
83
144
 
84
145
  - **File:** `packages/core/src/base/repositories/core/default-crud.ts`
85
146
 
86
- ### Example Implementation
147
+ ### @repository Decorator Requirements
148
+
149
+ **IMPORTANT:** Both `model` AND `dataSource` are required in the `@repository` decorator for schema auto-discovery. Without both, the model won't be registered and relational queries will fail.
87
150
 
88
151
  ```typescript
89
- // src/repositories/configuration.repository.ts
90
- import {
91
- Configuration,
92
- configurationRelations,
93
- TConfigurationSchema,
94
- } from '@/models/entities';
95
- import { IDataSource, inject, repository, DefaultCRUDRepository } from '@venizia/ignis';
96
-
97
- // Decorator to mark this class as a repository for DI
98
- @repository({})
99
- export class ConfigurationRepository extends DefaultCRUDRepository<TConfigurationSchema> {
152
+ // ❌ WRONG - Will throw error
153
+ @repository({ model: User }) // Missing dataSource!
154
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
155
+
156
+ // ❌ WRONG - Will throw error
157
+ @repository({ dataSource: PostgresDataSource }) // Missing model!
158
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
159
+
160
+ // CORRECT - Both model and dataSource provided
161
+ @repository({ model: User, dataSource: PostgresDataSource })
162
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
163
+ ```
164
+
165
+ ### Injection Patterns
166
+
167
+ The `@repository` decorator supports two injection patterns:
168
+
169
+ #### Pattern 1: Zero Boilerplate (Recommended)
170
+
171
+ DataSource is auto-injected from metadata - no constructor needed:
172
+
173
+ ```typescript
174
+ @repository({ model: User, dataSource: PostgresDataSource })
175
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
176
+ // No constructor needed - datasource auto-injected at param index 0
177
+
178
+ async findByEmail(email: string) {
179
+ return this.findOne({ filter: { where: { email } } });
180
+ }
181
+ }
182
+ ```
183
+
184
+ #### Pattern 2: Explicit @inject
185
+
186
+ When you need constructor control, use explicit `@inject`. **Important:** The first parameter must extend `AbstractDataSource` - this is enforced via reflection:
187
+
188
+ ```typescript
189
+ @repository({ model: User, dataSource: PostgresDataSource })
190
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
100
191
  constructor(
101
- // Inject the configured datasource
102
- @inject({ key: 'datasources.PostgresDataSource' }) dataSource: IDataSource,
192
+ @inject({ key: 'datasources.PostgresDataSource' })
193
+ dataSource: PostgresDataSource, // Must be concrete DataSource type, NOT 'any'
103
194
  ) {
104
- // Pass the datasource, the model's Entity class, and the relations definitions to the super constructor
105
- super({ dataSource, entityClass: Configuration, relations: configurationRelations.definitions });
195
+ super(dataSource);
106
196
  }
197
+ }
198
+ ```
199
+
200
+ **Note:** When `@inject` is at param index 0, auto-injection is skipped (your `@inject` takes precedence).
201
+
202
+ ### Constructor Type Validation
203
+
204
+ The framework validates constructor parameters at decorator time:
205
+
206
+ 1. **First parameter must extend `AbstractDataSource`** - Using `any`, `object`, or non-DataSource types will throw an error
207
+ 2. **Type compatibility check** - The constructor parameter type must be compatible with the `dataSource` specified in `@repository`
208
+
209
+ ```typescript
210
+ // ❌ Error: First parameter must extend AbstractDataSource | Received: 'Object'
211
+ @repository({ model: User, dataSource: PostgresDataSource })
212
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
213
+ constructor(
214
+ @inject({ key: 'datasources.PostgresDataSource' })
215
+ dataSource: any, // Will cause runtime error!
216
+ ) {
217
+ super(dataSource);
218
+ }
219
+ }
220
+
221
+ // ❌ Error: Type mismatch | Constructor expects 'MongoDataSource' but @repository specifies 'PostgresDataSource'
222
+ @repository({ model: User, dataSource: PostgresDataSource })
223
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
224
+ constructor(
225
+ @inject({ key: 'datasources.MongoDataSource' })
226
+ dataSource: MongoDataSource, // Wrong type!
227
+ ) {
228
+ super(dataSource);
229
+ }
230
+ }
231
+ ```
107
232
 
108
- // You can add custom data access methods here
233
+ ### Example Implementation
234
+
235
+ ```typescript
236
+ // src/repositories/configuration.repository.ts
237
+ import { Configuration, TConfigurationSchema } from '@/models/entities';
238
+ import { PostgresDataSource } from '@/datasources';
239
+ import { inject, repository, DefaultCRUDRepository } from '@venizia/ignis';
240
+
241
+ // Pattern 1: Zero boilerplate (recommended)
242
+ @repository({ model: Configuration, dataSource: PostgresDataSource })
243
+ export class ConfigurationRepository extends DefaultCRUDRepository<TConfigurationSchema> {
244
+ // No constructor needed - datasource and entity auto-resolved!
245
+
246
+ // Custom data access methods
109
247
  async findByCode(code: string): Promise<Configuration | undefined> {
110
- // 'this.connector' gives you direct access to the Drizzle instance
111
248
  const result = await this.connector.query.Configuration.findFirst({
112
249
  where: (table, { eq }) => eq(table.code, code)
113
250
  });
114
251
  return result;
115
252
  }
116
253
  }
254
+
255
+ // Pattern 2: With explicit constructor (when you need custom initialization)
256
+ @repository({ model: Configuration, dataSource: PostgresDataSource })
257
+ export class ConfigurationRepository extends DefaultCRUDRepository<TConfigurationSchema> {
258
+ constructor(
259
+ @inject({ key: 'datasources.PostgresDataSource' })
260
+ dataSource: PostgresDataSource,
261
+ ) {
262
+ super(dataSource); // Just pass dataSource - entity and relations auto-resolved!
263
+ }
264
+ }
117
265
  ```
266
+
118
267
  This architecture provides a clean and powerful abstraction for data access, separating the "how" of data fetching (Drizzle logic) from the "what" of business logic (services).
268
+
269
+ ## Advanced Features
270
+
271
+ ### Log Option for Debugging
272
+
273
+ All CRUD operations support a `log` option for debugging:
274
+
275
+ ```typescript
276
+ // Enable logging for a specific operation
277
+ await repo.create({
278
+ data: { name: 'John', email: 'john@example.com' },
279
+ options: {
280
+ log: { use: true, level: 'debug' }
281
+ }
282
+ });
283
+ // Output: [_create] Executing with opts: { data: [...], options: {...} }
284
+
285
+ // Available log levels: 'debug', 'info', 'warn', 'error'
286
+ await repo.updateById({
287
+ id: '123',
288
+ data: { name: 'Jane' },
289
+ options: { log: { use: true, level: 'info' } }
290
+ });
291
+ ```
292
+
293
+ **Available on:** `create`, `createAll`, `updateById`, `updateAll`, `deleteById`, `deleteAll`
294
+
295
+ ### TypeScript Return Type Inference
296
+
297
+ Repository methods now have improved type inference based on `shouldReturn`:
298
+
299
+ ```typescript
300
+ // When shouldReturn: false - TypeScript knows data is null
301
+ const result1 = await repo.create({
302
+ data: { name: 'John' },
303
+ options: { shouldReturn: false }
304
+ });
305
+ // Type: Promise<TCount & { data: null }>
306
+ console.log(result1.data); // null
307
+
308
+ // When shouldReturn: true (default) - TypeScript knows data is the entity
309
+ const result2 = await repo.create({
310
+ data: { name: 'John' },
311
+ options: { shouldReturn: true }
312
+ });
313
+ // Type: Promise<TCount & { data: User }>
314
+ console.log(result2.data.name); // 'John' - fully typed!
315
+
316
+ // Same for array operations
317
+ const results = await repo.createAll({
318
+ data: [{ name: 'John' }, { name: 'Jane' }],
319
+ options: { shouldReturn: true }
320
+ });
321
+ // Type: Promise<TCount & { data: User[] }>
322
+ ```
323
+
324
+ ### Relations Auto-Resolution
325
+
326
+ Relations are now automatically resolved from the entity's static `relations` property:
327
+
328
+ ```typescript
329
+ // Define entity with static relations
330
+ @model({ type: 'entity' })
331
+ export class User extends BaseEntity<typeof User.schema> {
332
+ static override schema = userTable;
333
+ static override relations = () => userRelations.definitions;
334
+ }
335
+
336
+ // Repository automatically uses entity's relations
337
+ @repository({ model: User, dataSource: PostgresDataSource })
338
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
339
+ // No need to pass relations in constructor - auto-resolved!
340
+ }
341
+
342
+ // Relations are available for include queries
343
+ const users = await repo.find({
344
+ filter: {
345
+ where: { status: 'active' },
346
+ include: [{ relation: 'posts' }], // Works automatically
347
+ }
348
+ });
349
+ ```
350
+
351
+ ### Query Interface Validation
352
+
353
+ The `getQueryInterface()` method validates that the entity's schema is properly registered:
354
+
355
+ ```typescript
356
+ // If schema key doesn't match, you get a helpful error:
357
+ // Error: [UserRepository] Schema key mismatch | Entity name 'User' not found in connector.query | Available keys: [Configuration, Post] | Ensure the model's TABLE_NAME matches the schema registration key
358
+ ```
359
+
360
+ ## Performance Optimizations
361
+
362
+ ### Core API for Flat Queries
363
+
364
+ The `ReadableRepository` automatically optimizes flat queries (no relations, no field selection) using Drizzle's Core API instead of Query API. This provides ~15-20% performance improvement for simple queries.
365
+
366
+ **Automatic Optimization:**
367
+
368
+ ```typescript
369
+ // This query is automatically optimized to use Core API
370
+ const users = await repo.find({
371
+ filter: {
372
+ where: { status: 'active' },
373
+ limit: 10,
374
+ order: ['createdAt DESC'],
375
+ }
376
+ });
377
+ // Uses: db.select().from(table).where(...).orderBy(...).limit(10)
378
+
379
+ // This query uses Query API (relations need relational mapper)
380
+ const usersWithPosts = await repo.find({
381
+ filter: {
382
+ where: { status: 'active' },
383
+ include: [{ relation: 'posts' }], // Has relations
384
+ }
385
+ });
386
+ // Uses: db.query.tableName.findMany({ with: { posts: true }, ... })
387
+ ```
388
+
389
+ **When Core API is used:**
390
+
391
+ | Filter Options | API Used | Reason |
392
+ |----------------|----------|--------|
393
+ | `where`, `limit`, `order`, `offset` only | Core API | Flat query, no overhead |
394
+ | Has `include` (relations) | Query API | Needs relational mapper |
395
+ | Has `fields` selection | Query API | Core API field syntax differs |
396
+
397
+ **Protected Helper Method:**
398
+
399
+ For advanced use cases, you can directly use the `findWithCoreAPI` method:
400
+
401
+ ```typescript
402
+ // Available in subclasses
403
+ protected async findWithCoreAPI(opts: {
404
+ filter: TFilter<DataObject>;
405
+ findOne?: boolean;
406
+ }): Promise<Array<DataObject>>;
407
+
408
+ // Check if Core API can be used
409
+ protected canUseCoreAPI(filter: TFilter<DataObject>): boolean;
410
+ ```
411
+
412
+ ### WeakMap Cache for Filter Builder
413
+
414
+ The `DrizzleFilterBuilder` uses a static WeakMap cache for `getTableColumns()` results, avoiding repeated reflection calls:
415
+
416
+ ```typescript
417
+ // Internal optimization - no action needed
418
+ // First call: getTableColumns(schema) → cached
419
+ // Subsequent calls: retrieved from WeakMap
420
+ ```
421
+
422
+ This is especially beneficial for:
423
+ - High-concurrency environments
424
+ - Queries with nested AND/OR conditions (each recursion reuses cache)
425
+ - Multiple queries to the same table
426
+
427
+ ## Query Operators
428
+
429
+ The filter builder supports a comprehensive set of query operators for building complex queries.
430
+
431
+ **File:** `packages/core/src/base/repositories/operators/query.ts`
432
+
433
+ ### Available Operators
434
+
435
+ | Operator | Alias | SQL Equivalent | Description |
436
+ |----------|-------|----------------|-------------|
437
+ | `eq` | - | `=` | Equal to |
438
+ | `ne` | `neq` | `!=` | Not equal to |
439
+ | `gt` | - | `>` | Greater than |
440
+ | `gte` | - | `>=` | Greater than or equal |
441
+ | `lt` | - | `<` | Less than |
442
+ | `lte` | - | `<=` | Less than or equal |
443
+ | `like` | - | `LIKE` | Pattern matching (case-sensitive) |
444
+ | `nlike` | - | `NOT LIKE` | Negative pattern matching |
445
+ | `ilike` | - | `ILIKE` | Pattern matching (case-insensitive, PostgreSQL) |
446
+ | `nilike` | - | `NOT ILIKE` | Negative case-insensitive pattern |
447
+ | `in` | `inq` | `IN` | Value in array |
448
+ | `nin` | - | `NOT IN` | Value not in array |
449
+ | `between` | - | `BETWEEN` | Value between two values |
450
+ | `is` | - | `IS NULL` | Null check |
451
+ | `isn` | - | `IS NOT NULL` | Not null check |
452
+ | `regexp` | - | `~` | PostgreSQL POSIX regex (case-sensitive) |
453
+ | `iregexp` | - | `~*` | PostgreSQL POSIX regex (case-insensitive) |
454
+
455
+ ### Logical Operators
456
+
457
+ | Operator | Description |
458
+ |----------|-------------|
459
+ | `and` | Combine conditions with AND |
460
+ | `or` | Combine conditions with OR |
461
+
462
+ ### Usage Examples
463
+
464
+ **Simple equality:**
465
+ ```typescript
466
+ await repo.find({ filter: { where: { status: 'active' } } });
467
+ // SQL: WHERE status = 'active'
468
+ ```
469
+
470
+ **Comparison operators:**
471
+ ```typescript
472
+ await repo.find({
473
+ filter: {
474
+ where: {
475
+ age: { gte: 18, lt: 65 },
476
+ score: { gt: 100 }
477
+ }
478
+ }
479
+ });
480
+ // SQL: WHERE age >= 18 AND age < 65 AND score > 100
481
+ ```
482
+
483
+ **Array operators:**
484
+ ```typescript
485
+ // IN operator
486
+ await repo.find({ filter: { where: { id: [1, 2, 3] } } });
487
+ // SQL: WHERE id IN (1, 2, 3)
488
+
489
+ // Using explicit IN
490
+ await repo.find({ filter: { where: { status: { in: ['active', 'pending'] } } } });
491
+
492
+ // NOT IN
493
+ await repo.find({ filter: { where: { status: { nin: ['deleted', 'archived'] } } } });
494
+ ```
495
+
496
+ **Pattern matching:**
497
+ ```typescript
498
+ // LIKE (case-sensitive)
499
+ await repo.find({ filter: { where: { name: { like: '%john%' } } } });
500
+
501
+ // ILIKE (case-insensitive, PostgreSQL)
502
+ await repo.find({ filter: { where: { email: { ilike: '%@gmail.com' } } } });
503
+ ```
504
+
505
+ **Regex (PostgreSQL):**
506
+ ```typescript
507
+ // Case-sensitive regex
508
+ await repo.find({ filter: { where: { name: { regexp: '^John' } } } });
509
+
510
+ // Case-insensitive regex
511
+ await repo.find({ filter: { where: { name: { iregexp: '^john' } } } });
512
+ ```
513
+
514
+ **Between:**
515
+ ```typescript
516
+ await repo.find({
517
+ filter: {
518
+ where: {
519
+ createdAt: { between: [new Date('2024-01-01'), new Date('2024-12-31')] }
520
+ }
521
+ }
522
+ });
523
+ // SQL: WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'
524
+ ```
525
+
526
+ **Logical operators:**
527
+ ```typescript
528
+ // OR conditions
529
+ await repo.find({
530
+ filter: {
531
+ where: {
532
+ or: [
533
+ { status: 'active' },
534
+ { isPublished: true }
535
+ ]
536
+ }
537
+ }
538
+ });
539
+
540
+ // AND conditions (explicit)
541
+ await repo.find({
542
+ filter: {
543
+ where: {
544
+ and: [
545
+ { role: 'admin' },
546
+ { createdAt: { gte: new Date('2024-01-01') } }
547
+ ]
548
+ }
549
+ }
550
+ });
551
+
552
+ // Nested conditions
553
+ await repo.find({
554
+ filter: {
555
+ where: {
556
+ status: 'active',
557
+ or: [
558
+ { role: 'admin' },
559
+ { and: [{ role: 'user' }, { verified: true }] }
560
+ ]
561
+ }
562
+ }
563
+ });
564
+ ```
565
+
566
+ ### Security Notes
567
+
568
+ - **Empty IN array:** Returns `false` (no rows), preventing security bypass
569
+ - **Empty NOT IN array:** Returns `true` (all rows match)
570
+ - **BETWEEN validation:** Requires exactly 2 elements in array, throws error otherwise
571
+ - **Invalid columns:** Throws error if column doesn't exist in schema