@ooneex/repository 0.0.1 → 0.0.4
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.
- package/README.md +663 -0
- package/dist/index.d.ts +6 -12
- package/dist/index.js +2 -1
- package/dist/index.js.map +4 -3
- package/package.json +19 -8
- package/dist/ooneex-repository-0.0.1.tgz +0 -0
package/README.md
CHANGED
|
@@ -1 +1,664 @@
|
|
|
1
1
|
# @ooneex/repository
|
|
2
|
+
|
|
3
|
+
A base repository decorator and interface for data access layer abstraction in TypeScript applications. This package provides the foundation for creating injectable repository classes with dependency injection support, standardized CRUD operations, and paginated query results.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
✅ **Repository Decorator** - Register repositories with the DI container using decorators
|
|
15
|
+
|
|
16
|
+
✅ **Interface Contract** - Standard interface for repository implementations
|
|
17
|
+
|
|
18
|
+
✅ **Paginated Results** - Built-in support for paginated query results
|
|
19
|
+
|
|
20
|
+
✅ **Scope Control** - Configure singleton, transient, or request-scoped repositories
|
|
21
|
+
|
|
22
|
+
✅ **Type-Safe** - Full TypeScript support with generic entity and criteria types
|
|
23
|
+
|
|
24
|
+
✅ **Container Integration** - Seamless integration with @ooneex/container
|
|
25
|
+
|
|
26
|
+
✅ **Search Support** - Built-in query string parameter for search functionality
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### Bun
|
|
31
|
+
```bash
|
|
32
|
+
bun add @ooneex/repository
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### pnpm
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add @ooneex/repository
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Yarn
|
|
41
|
+
```bash
|
|
42
|
+
yarn add @ooneex/repository
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### npm
|
|
46
|
+
```bash
|
|
47
|
+
npm install @ooneex/repository
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Basic Repository
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { decorator, type IRepository } from '@ooneex/repository';
|
|
56
|
+
import type { FilterResultType } from '@ooneex/types';
|
|
57
|
+
|
|
58
|
+
interface User {
|
|
59
|
+
id: string;
|
|
60
|
+
email: string;
|
|
61
|
+
name: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface UserCriteria {
|
|
65
|
+
email?: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
isActive?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@decorator.repository()
|
|
71
|
+
class UserRepository implements IRepository<User, UserCriteria> {
|
|
72
|
+
private connection: unknown = null;
|
|
73
|
+
|
|
74
|
+
public async open(): Promise<unknown> {
|
|
75
|
+
// Open database connection
|
|
76
|
+
this.connection = await this.createConnection();
|
|
77
|
+
return this.connection;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async close(): Promise<void> {
|
|
81
|
+
// Close database connection
|
|
82
|
+
this.connection = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public async find(
|
|
86
|
+
criteria: UserCriteria & { page?: number; limit?: number; q?: string }
|
|
87
|
+
): Promise<FilterResultType<User>> {
|
|
88
|
+
const { page = 1, limit = 10, q, ...filters } = criteria;
|
|
89
|
+
|
|
90
|
+
// Query implementation
|
|
91
|
+
const users = await this.queryUsers(filters, q);
|
|
92
|
+
const total = await this.countUsers(filters, q);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
resources: users,
|
|
96
|
+
total,
|
|
97
|
+
totalPages: Math.ceil(total / limit),
|
|
98
|
+
page,
|
|
99
|
+
limit
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async createConnection(): Promise<unknown> {
|
|
104
|
+
// Connection logic
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async queryUsers(filters: UserCriteria, q?: string): Promise<User[]> {
|
|
109
|
+
// Query logic
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async countUsers(filters: UserCriteria, q?: string): Promise<number> {
|
|
114
|
+
// Count logic
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Resolving Repositories
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { container } from '@ooneex/container';
|
|
124
|
+
import { UserRepository } from './repositories/UserRepository';
|
|
125
|
+
|
|
126
|
+
// Repository is automatically registered by the decorator
|
|
127
|
+
const userRepo = container.get(UserRepository);
|
|
128
|
+
|
|
129
|
+
// Open connection
|
|
130
|
+
await userRepo.open();
|
|
131
|
+
|
|
132
|
+
// Find users with pagination
|
|
133
|
+
const result = await userRepo.find({
|
|
134
|
+
isActive: true,
|
|
135
|
+
page: 1,
|
|
136
|
+
limit: 20,
|
|
137
|
+
q: 'john'
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(result.resources); // User[]
|
|
141
|
+
console.log(result.total); // Total count
|
|
142
|
+
console.log(result.totalPages); // Number of pages
|
|
143
|
+
|
|
144
|
+
// Close connection
|
|
145
|
+
await userRepo.close();
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Repository with TypeORM
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { decorator, type IRepository } from '@ooneex/repository';
|
|
152
|
+
import { container } from '@ooneex/container';
|
|
153
|
+
import type { FilterResultType } from '@ooneex/types';
|
|
154
|
+
import type { IDatabase } from '@ooneex/database';
|
|
155
|
+
import type { Repository } from 'typeorm';
|
|
156
|
+
|
|
157
|
+
interface Product {
|
|
158
|
+
id: string;
|
|
159
|
+
name: string;
|
|
160
|
+
price: number;
|
|
161
|
+
category: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface ProductCriteria {
|
|
165
|
+
category?: string;
|
|
166
|
+
minPrice?: number;
|
|
167
|
+
maxPrice?: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@decorator.repository()
|
|
171
|
+
class ProductRepository implements IRepository<Product, ProductCriteria> {
|
|
172
|
+
private readonly database = container.get<IDatabase>('database');
|
|
173
|
+
private repository: Repository<Product> | null = null;
|
|
174
|
+
|
|
175
|
+
public async open(): Promise<Repository<Product>> {
|
|
176
|
+
this.repository = await this.database.open(ProductEntity);
|
|
177
|
+
return this.repository;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public async close(): Promise<void> {
|
|
181
|
+
// TypeORM manages connection lifecycle
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public async find(
|
|
185
|
+
criteria: ProductCriteria & { page?: number; limit?: number; q?: string }
|
|
186
|
+
): Promise<FilterResultType<Product>> {
|
|
187
|
+
const { page = 1, limit = 10, q, category, minPrice, maxPrice } = criteria;
|
|
188
|
+
|
|
189
|
+
const queryBuilder = this.repository!
|
|
190
|
+
.createQueryBuilder('product');
|
|
191
|
+
|
|
192
|
+
if (category) {
|
|
193
|
+
queryBuilder.andWhere('product.category = :category', { category });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (minPrice !== undefined) {
|
|
197
|
+
queryBuilder.andWhere('product.price >= :minPrice', { minPrice });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (maxPrice !== undefined) {
|
|
201
|
+
queryBuilder.andWhere('product.price <= :maxPrice', { maxPrice });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (q) {
|
|
205
|
+
queryBuilder.andWhere('product.name ILIKE :q', { q: `%${q}%` });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const [resources, total] = await queryBuilder
|
|
209
|
+
.skip((page - 1) * limit)
|
|
210
|
+
.take(limit)
|
|
211
|
+
.getManyAndCount();
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
resources,
|
|
215
|
+
total,
|
|
216
|
+
totalPages: Math.ceil(total / limit),
|
|
217
|
+
page,
|
|
218
|
+
limit
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Transient Repositories
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { decorator, type IRepository } from '@ooneex/repository';
|
|
228
|
+
import { EContainerScope } from '@ooneex/container';
|
|
229
|
+
|
|
230
|
+
@decorator.repository(EContainerScope.Transient)
|
|
231
|
+
class TransientRepository implements IRepository {
|
|
232
|
+
private readonly instanceId = crypto.randomUUID();
|
|
233
|
+
|
|
234
|
+
public async open(): Promise<unknown> {
|
|
235
|
+
console.log(`Opening connection for instance: ${this.instanceId}`);
|
|
236
|
+
return {};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public async close(): Promise<void> {
|
|
240
|
+
console.log(`Closing connection for instance: ${this.instanceId}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public async find(criteria: { page?: number; limit?: number; q?: string }) {
|
|
244
|
+
return {
|
|
245
|
+
resources: [],
|
|
246
|
+
total: 0,
|
|
247
|
+
totalPages: 0,
|
|
248
|
+
page: criteria.page ?? 1,
|
|
249
|
+
limit: criteria.limit ?? 10
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Each resolution creates a new instance
|
|
255
|
+
const repo1 = container.get(TransientRepository);
|
|
256
|
+
const repo2 = container.get(TransientRepository);
|
|
257
|
+
// repo1 and repo2 have different instanceIds
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## API Reference
|
|
261
|
+
|
|
262
|
+
### Decorators
|
|
263
|
+
|
|
264
|
+
#### `@decorator.repository(scope?)`
|
|
265
|
+
|
|
266
|
+
Decorator to register a repository class with the DI container.
|
|
267
|
+
|
|
268
|
+
**Parameters:**
|
|
269
|
+
- `scope` - Container scope (default: `EContainerScope.Singleton`)
|
|
270
|
+
- `Singleton` - Single instance shared across all requests
|
|
271
|
+
- `Transient` - New instance created on every resolution
|
|
272
|
+
- `Request` - New instance per request context
|
|
273
|
+
|
|
274
|
+
**Example:**
|
|
275
|
+
```typescript
|
|
276
|
+
import { decorator } from '@ooneex/repository';
|
|
277
|
+
import { EContainerScope } from '@ooneex/container';
|
|
278
|
+
|
|
279
|
+
// Singleton (default)
|
|
280
|
+
@decorator.repository()
|
|
281
|
+
class MySingletonRepository {}
|
|
282
|
+
|
|
283
|
+
// Transient
|
|
284
|
+
@decorator.repository(EContainerScope.Transient)
|
|
285
|
+
class MyTransientRepository {}
|
|
286
|
+
|
|
287
|
+
// Request-scoped
|
|
288
|
+
@decorator.repository(EContainerScope.Request)
|
|
289
|
+
class MyRequestRepository {}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Interfaces
|
|
293
|
+
|
|
294
|
+
#### `IRepository<T, TCriteria>`
|
|
295
|
+
|
|
296
|
+
Interface for repository implementations.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
interface IRepository<T = unknown, TCriteria = unknown> {
|
|
300
|
+
open: () => Promise<unknown>;
|
|
301
|
+
close: () => Promise<void>;
|
|
302
|
+
find: (criteria: TCriteria & { page?: number; limit?: number; q?: string }) => Promise<FilterResultType<T>>;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Type Parameters:**
|
|
307
|
+
- `T` - The entity type returned by the repository
|
|
308
|
+
- `TCriteria` - The criteria type for filtering results
|
|
309
|
+
|
|
310
|
+
**Methods:**
|
|
311
|
+
|
|
312
|
+
##### `open(): Promise<unknown>`
|
|
313
|
+
|
|
314
|
+
Open or initialize the data source connection.
|
|
315
|
+
|
|
316
|
+
**Returns:** Promise resolving to the connection or data source
|
|
317
|
+
|
|
318
|
+
##### `close(): Promise<void>`
|
|
319
|
+
|
|
320
|
+
Close the data source connection.
|
|
321
|
+
|
|
322
|
+
**Returns:** Promise that resolves when connection is closed
|
|
323
|
+
|
|
324
|
+
##### `find(criteria): Promise<FilterResultType<T>>`
|
|
325
|
+
|
|
326
|
+
Find entities matching the given criteria with pagination support.
|
|
327
|
+
|
|
328
|
+
**Parameters:**
|
|
329
|
+
- `criteria` - Filter criteria including:
|
|
330
|
+
- Custom filter fields from `TCriteria`
|
|
331
|
+
- `page` - Page number (default: 1)
|
|
332
|
+
- `limit` - Items per page (default: 10)
|
|
333
|
+
- `q` - Search query string
|
|
334
|
+
|
|
335
|
+
**Returns:** Promise resolving to paginated results
|
|
336
|
+
|
|
337
|
+
### Types
|
|
338
|
+
|
|
339
|
+
#### `RepositoryClassType`
|
|
340
|
+
|
|
341
|
+
Type for repository class constructors.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
type RepositoryClassType = new (...args: any[]) => IRepository;
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### `FilterResultType<T>`
|
|
348
|
+
|
|
349
|
+
Type for paginated query results (from @ooneex/types).
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
type FilterResultType<T> = {
|
|
353
|
+
resources: T[];
|
|
354
|
+
total: number;
|
|
355
|
+
totalPages: number;
|
|
356
|
+
page: number;
|
|
357
|
+
limit: number;
|
|
358
|
+
};
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Advanced Usage
|
|
362
|
+
|
|
363
|
+
### Repository with Caching
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { decorator, type IRepository } from '@ooneex/repository';
|
|
367
|
+
import { container } from '@ooneex/container';
|
|
368
|
+
import type { ICache } from '@ooneex/cache';
|
|
369
|
+
import type { FilterResultType } from '@ooneex/types';
|
|
370
|
+
|
|
371
|
+
interface Article {
|
|
372
|
+
id: string;
|
|
373
|
+
title: string;
|
|
374
|
+
content: string;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@decorator.repository()
|
|
378
|
+
class CachedArticleRepository implements IRepository<Article> {
|
|
379
|
+
private readonly cache = container.get<ICache>('cache');
|
|
380
|
+
|
|
381
|
+
public async open(): Promise<unknown> {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
public async close(): Promise<void> {}
|
|
386
|
+
|
|
387
|
+
public async find(
|
|
388
|
+
criteria: { page?: number; limit?: number; q?: string }
|
|
389
|
+
): Promise<FilterResultType<Article>> {
|
|
390
|
+
const cacheKey = `articles:${JSON.stringify(criteria)}`;
|
|
391
|
+
|
|
392
|
+
// Check cache first
|
|
393
|
+
const cached = await this.cache.get<FilterResultType<Article>>(cacheKey);
|
|
394
|
+
if (cached) {
|
|
395
|
+
return cached;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Query database
|
|
399
|
+
const result = await this.queryDatabase(criteria);
|
|
400
|
+
|
|
401
|
+
// Cache for 5 minutes
|
|
402
|
+
await this.cache.set(cacheKey, result, 300);
|
|
403
|
+
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async queryDatabase(
|
|
408
|
+
criteria: { page?: number; limit?: number; q?: string }
|
|
409
|
+
): Promise<FilterResultType<Article>> {
|
|
410
|
+
// Database query logic
|
|
411
|
+
return {
|
|
412
|
+
resources: [],
|
|
413
|
+
total: 0,
|
|
414
|
+
totalPages: 0,
|
|
415
|
+
page: criteria.page ?? 1,
|
|
416
|
+
limit: criteria.limit ?? 10
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Repository with Logging
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { decorator, type IRepository } from '@ooneex/repository';
|
|
426
|
+
import { container } from '@ooneex/container';
|
|
427
|
+
import type { ILogger } from '@ooneex/logger';
|
|
428
|
+
import type { FilterResultType } from '@ooneex/types';
|
|
429
|
+
|
|
430
|
+
@decorator.repository()
|
|
431
|
+
class LoggedRepository<T> implements IRepository<T> {
|
|
432
|
+
private readonly logger = container.get<ILogger>('logger');
|
|
433
|
+
|
|
434
|
+
public async open(): Promise<unknown> {
|
|
435
|
+
this.logger.info('Opening repository connection');
|
|
436
|
+
const connection = await this.createConnection();
|
|
437
|
+
this.logger.success('Repository connection opened');
|
|
438
|
+
return connection;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
public async close(): Promise<void> {
|
|
442
|
+
this.logger.info('Closing repository connection');
|
|
443
|
+
await this.closeConnection();
|
|
444
|
+
this.logger.success('Repository connection closed');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
public async find(
|
|
448
|
+
criteria: { page?: number; limit?: number; q?: string }
|
|
449
|
+
): Promise<FilterResultType<T>> {
|
|
450
|
+
this.logger.info('Finding resources', { criteria });
|
|
451
|
+
|
|
452
|
+
const startTime = Date.now();
|
|
453
|
+
const result = await this.queryResources(criteria);
|
|
454
|
+
const duration = Date.now() - startTime;
|
|
455
|
+
|
|
456
|
+
this.logger.info('Found resources', {
|
|
457
|
+
total: result.total,
|
|
458
|
+
page: result.page,
|
|
459
|
+
duration: `${duration}ms`
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private async createConnection(): Promise<unknown> {
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private async closeConnection(): Promise<void> {}
|
|
470
|
+
|
|
471
|
+
private async queryResources(
|
|
472
|
+
criteria: { page?: number; limit?: number; q?: string }
|
|
473
|
+
): Promise<FilterResultType<T>> {
|
|
474
|
+
return {
|
|
475
|
+
resources: [],
|
|
476
|
+
total: 0,
|
|
477
|
+
totalPages: 0,
|
|
478
|
+
page: criteria.page ?? 1,
|
|
479
|
+
limit: criteria.limit ?? 10
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Abstract Base Repository
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { type IRepository } from '@ooneex/repository';
|
|
489
|
+
import type { FilterResultType } from '@ooneex/types';
|
|
490
|
+
|
|
491
|
+
abstract class BaseRepository<T, TCriteria = Record<string, unknown>>
|
|
492
|
+
implements IRepository<T, TCriteria> {
|
|
493
|
+
|
|
494
|
+
protected connection: unknown = null;
|
|
495
|
+
|
|
496
|
+
public async open(): Promise<unknown> {
|
|
497
|
+
this.connection = await this.createConnection();
|
|
498
|
+
return this.connection;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
public async close(): Promise<void> {
|
|
502
|
+
await this.closeConnection();
|
|
503
|
+
this.connection = null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
public abstract find(
|
|
507
|
+
criteria: TCriteria & { page?: number; limit?: number; q?: string }
|
|
508
|
+
): Promise<FilterResultType<T>>;
|
|
509
|
+
|
|
510
|
+
protected abstract createConnection(): Promise<unknown>;
|
|
511
|
+
protected abstract closeConnection(): Promise<void>;
|
|
512
|
+
|
|
513
|
+
protected paginate<R>(
|
|
514
|
+
resources: R[],
|
|
515
|
+
total: number,
|
|
516
|
+
page: number,
|
|
517
|
+
limit: number
|
|
518
|
+
): FilterResultType<R> {
|
|
519
|
+
return {
|
|
520
|
+
resources,
|
|
521
|
+
total,
|
|
522
|
+
totalPages: Math.ceil(total / limit),
|
|
523
|
+
page,
|
|
524
|
+
limit
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Usage
|
|
530
|
+
@decorator.repository()
|
|
531
|
+
class OrderRepository extends BaseRepository<Order, OrderCriteria> {
|
|
532
|
+
protected async createConnection(): Promise<unknown> {
|
|
533
|
+
// Implementation
|
|
534
|
+
return {};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
protected async closeConnection(): Promise<void> {
|
|
538
|
+
// Implementation
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
public async find(
|
|
542
|
+
criteria: OrderCriteria & { page?: number; limit?: number; q?: string }
|
|
543
|
+
): Promise<FilterResultType<Order>> {
|
|
544
|
+
const { page = 1, limit = 10 } = criteria;
|
|
545
|
+
|
|
546
|
+
const orders = await this.queryOrders(criteria);
|
|
547
|
+
const total = await this.countOrders(criteria);
|
|
548
|
+
|
|
549
|
+
return this.paginate(orders, total, page, limit);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Controller Integration
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import { Route } from '@ooneex/routing';
|
|
558
|
+
import { container } from '@ooneex/container';
|
|
559
|
+
import type { IController, ContextType } from '@ooneex/controller';
|
|
560
|
+
import { UserRepository } from './repositories/UserRepository';
|
|
561
|
+
|
|
562
|
+
@Route.http({
|
|
563
|
+
name: 'api.users.list',
|
|
564
|
+
path: '/api/users',
|
|
565
|
+
method: 'GET',
|
|
566
|
+
description: 'List users with pagination'
|
|
567
|
+
})
|
|
568
|
+
class UserListController implements IController {
|
|
569
|
+
private readonly userRepository = container.get(UserRepository);
|
|
570
|
+
|
|
571
|
+
public async index(context: ContextType): Promise<IResponse> {
|
|
572
|
+
const { page, limit, q, ...filters } = context.queries;
|
|
573
|
+
|
|
574
|
+
const result = await this.userRepository.find({
|
|
575
|
+
...filters,
|
|
576
|
+
page: Number(page) || 1,
|
|
577
|
+
limit: Number(limit) || 10,
|
|
578
|
+
q: q as string
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
return context.response.json(result);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Testing Repositories
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
590
|
+
import { container } from '@ooneex/container';
|
|
591
|
+
import { UserRepository } from './UserRepository';
|
|
592
|
+
|
|
593
|
+
describe('UserRepository', () => {
|
|
594
|
+
let repository: UserRepository;
|
|
595
|
+
|
|
596
|
+
beforeEach(async () => {
|
|
597
|
+
repository = container.get(UserRepository);
|
|
598
|
+
await repository.open();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
afterEach(async () => {
|
|
602
|
+
await repository.close();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test('should return paginated results', async () => {
|
|
606
|
+
const result = await repository.find({ page: 1, limit: 10 });
|
|
607
|
+
|
|
608
|
+
expect(result).toHaveProperty('resources');
|
|
609
|
+
expect(result).toHaveProperty('total');
|
|
610
|
+
expect(result).toHaveProperty('totalPages');
|
|
611
|
+
expect(result).toHaveProperty('page', 1);
|
|
612
|
+
expect(result).toHaveProperty('limit', 10);
|
|
613
|
+
expect(Array.isArray(result.resources)).toBe(true);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test('should filter by criteria', async () => {
|
|
617
|
+
const result = await repository.find({
|
|
618
|
+
isActive: true,
|
|
619
|
+
page: 1,
|
|
620
|
+
limit: 10
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(result.resources.every(user => user.isActive)).toBe(true);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test('should search with query string', async () => {
|
|
627
|
+
const result = await repository.find({
|
|
628
|
+
q: 'john',
|
|
629
|
+
page: 1,
|
|
630
|
+
limit: 10
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
expect(result.resources.every(
|
|
634
|
+
user => user.name.toLowerCase().includes('john')
|
|
635
|
+
)).toBe(true);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## License
|
|
641
|
+
|
|
642
|
+
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|
|
643
|
+
|
|
644
|
+
## Contributing
|
|
645
|
+
|
|
646
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
647
|
+
|
|
648
|
+
### Development Setup
|
|
649
|
+
|
|
650
|
+
1. Clone the repository
|
|
651
|
+
2. Install dependencies: `bun install`
|
|
652
|
+
3. Run tests: `bun run test`
|
|
653
|
+
4. Build the project: `bun run build`
|
|
654
|
+
|
|
655
|
+
### Guidelines
|
|
656
|
+
|
|
657
|
+
- Write tests for new features
|
|
658
|
+
- Follow the existing code style
|
|
659
|
+
- Update documentation for API changes
|
|
660
|
+
- Ensure all tests pass before submitting PR
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
Made with ❤️ by the Ooneex team
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { EContainerScope } from "@ooneex/container";
|
|
1
2
|
import { FilterResultType } from "@ooneex/types";
|
|
2
3
|
type RepositoryClassType = new (...args: any[]) => IRepository;
|
|
3
4
|
interface IRepository<
|
|
4
5
|
T = unknown,
|
|
5
|
-
TCriteria = unknown
|
|
6
|
-
TSaveOptions = unknown,
|
|
7
|
-
TDeleteResult = unknown
|
|
6
|
+
TCriteria = unknown
|
|
8
7
|
> {
|
|
9
8
|
open: () => Promise<unknown>;
|
|
10
9
|
close: () => Promise<void>;
|
|
@@ -13,13 +12,8 @@ interface IRepository<
|
|
|
13
12
|
limit?: number;
|
|
14
13
|
q?: string;
|
|
15
14
|
}) => Promise<FilterResultType<T>>;
|
|
16
|
-
findOne: (id: string) => Promise<T | null>;
|
|
17
|
-
findOneBy: (criteria: TCriteria) => Promise<T | null>;
|
|
18
|
-
create: (entity: T, options?: TSaveOptions) => Promise<T>;
|
|
19
|
-
createMany: (entities: T[], options?: TSaveOptions) => Promise<T[]>;
|
|
20
|
-
update: (entity: T, options?: TSaveOptions) => Promise<T>;
|
|
21
|
-
updateMany: (entities: T[], options?: TSaveOptions) => Promise<T[]>;
|
|
22
|
-
delete: (criteria: TCriteria | TCriteria[]) => Promise<TDeleteResult>;
|
|
23
|
-
count: (criteria?: TCriteria | TCriteria[]) => Promise<number>;
|
|
24
15
|
}
|
|
25
|
-
|
|
16
|
+
declare const decorator: {
|
|
17
|
+
repository: (scope?: EContainerScope) => (target: RepositoryClassType) => void;
|
|
18
|
+
};
|
|
19
|
+
export { decorator, RepositoryClassType, IRepository };
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [],
|
|
3
|
+
"sources": ["src/decorators.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
+
"import { container, EContainerScope } from \"@ooneex/container\";\nimport type { RepositoryClassType } from \"./types\";\n\nexport const decorator = {\n repository: (scope: EContainerScope = EContainerScope.Singleton) => {\n return (target: RepositoryClassType): void => {\n container.add(target, scope);\n };\n },\n};\n"
|
|
5
6
|
],
|
|
6
|
-
"mappings": "",
|
|
7
|
-
"debugId": "
|
|
7
|
+
"mappings": "AAAA,oBAAS,qBAAW,0BAGb,IAAM,EAAY,CACvB,WAAY,CAAC,EAAyB,EAAgB,YAAc,CAClE,MAAO,CAAC,IAAsC,CAC5C,EAAU,IAAI,EAAQ,CAAK,GAGjC",
|
|
8
|
+
"debugId": "1D18A0E1C9F4487464756E2164756E21",
|
|
8
9
|
"names": []
|
|
9
10
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ooneex/repository",
|
|
3
|
-
"description": "",
|
|
4
|
-
"version": "0.0.
|
|
3
|
+
"description": "Base repository classes and decorators for data access layer with dependency injection support",
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -25,11 +25,22 @@
|
|
|
25
25
|
"test": "bun test tests",
|
|
26
26
|
"build": "bunup",
|
|
27
27
|
"lint": "tsgo --noEmit && bunx biome lint",
|
|
28
|
-
"publish
|
|
29
|
-
"publish:pack": "bun pm pack --destination ./dist",
|
|
30
|
-
"publish:dry": "bun publish --dry-run"
|
|
28
|
+
"publish": "bun publish --access public || true"
|
|
31
29
|
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@ooneex/container": "0.0.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@ooneex/types": "0.0.1"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"bun",
|
|
38
|
+
"dao",
|
|
39
|
+
"data-access",
|
|
40
|
+
"decorator",
|
|
41
|
+
"ooneex",
|
|
42
|
+
"pattern",
|
|
43
|
+
"repository",
|
|
44
|
+
"typescript"
|
|
45
|
+
]
|
|
35
46
|
}
|
|
Binary file
|