@di-framework/di-framework-repo 0.1.0
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 +107 -0
- package/dist/adapter.d.ts +70 -0
- package/dist/adapter.js +1 -0
- package/dist/decorators.d.ts +12 -0
- package/dist/decorators.js +17 -0
- package/dist/in-memory.d.ts +19 -0
- package/dist/in-memory.js +47 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/repository.d.ts +50 -0
- package/dist/repository.js +51 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +1 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @geoffsee/df-repo
|
|
2
|
+
|
|
3
|
+
A coherent abstraction of repositories and storage adapters for TypeScript, with optional integration for `di-framework`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Storage Agnostic**: Decouples your business logic from the underlying storage technology (SQL, NoSQL, In-Memory, etc.).
|
|
8
|
+
- **Standardized Patterns**: Provides `BaseRepository`, `EntityRepository`, and `SoftDeleteRepository` to handle common data access patterns.
|
|
9
|
+
- **Built-in Pagination**: Standardized `Page` and `PaginatedResult` types with built-in support in adapters and repositories.
|
|
10
|
+
- **In-Memory Implementation**: Includes a fully functional `InMemoryRepository` for prototyping and testing.
|
|
11
|
+
- **DI Integration**: Seamlessly integrates with `di-framework` via the `@Repository` decorator.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @geoffsee/df-repo
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Optional: If you want to use the `@Repository` decorator for dependency injection, ensure you have `di-framework` installed.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add di-framework
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Basic Usage
|
|
26
|
+
|
|
27
|
+
### 1. Define your Entity
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
interface User {
|
|
31
|
+
id: number;
|
|
32
|
+
name: string;
|
|
33
|
+
email: string;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Implement a Repository
|
|
38
|
+
|
|
39
|
+
You can extend `InMemoryRepository` for quick prototyping:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { InMemoryRepository } from '@geoffsee/df-repo';
|
|
43
|
+
|
|
44
|
+
class UserRepository extends InMemoryRepository<User, number> {
|
|
45
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
46
|
+
const all = await this.findAll();
|
|
47
|
+
return all.find(u => u.email === email) || null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Use with di-framework
|
|
53
|
+
|
|
54
|
+
Use the `@Repository` decorator to automatically register your repository with the `di-framework` container.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Repository } from '@geoffsee/df-repo';
|
|
58
|
+
|
|
59
|
+
@Repository()
|
|
60
|
+
class UserRepository extends InMemoryRepository<User, number> {
|
|
61
|
+
// ...
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// In another service
|
|
65
|
+
@Container()
|
|
66
|
+
class UserService {
|
|
67
|
+
constructor(@Component(UserRepository) private users: UserRepository) {}
|
|
68
|
+
|
|
69
|
+
async listUsers() {
|
|
70
|
+
return this.users.findAll();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Storage Adapters
|
|
76
|
+
|
|
77
|
+
The `StorageAdapter` interface allows you to implement custom backends.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { StorageAdapter, BaseRepository } from '@geoffsee/df-repo';
|
|
81
|
+
|
|
82
|
+
class MyCustomAdapter<E, ID> implements StorageAdapter<E, ID> {
|
|
83
|
+
// Implement findById, save, delete, findPaginated, etc.
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class MyRepository extends BaseRepository<User, number> {
|
|
87
|
+
constructor(adapter: MyCustomAdapter<User, number>) {
|
|
88
|
+
super(adapter);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API Overview
|
|
94
|
+
|
|
95
|
+
### Repository Classes
|
|
96
|
+
- `BaseRepository<E, ID>`: The foundational repository class.
|
|
97
|
+
- `EntityRepository<E, ID>`: Standard entity-aware repository.
|
|
98
|
+
- `SoftDeleteRepository<E, ID>`: Repository with soft-delete capabilities.
|
|
99
|
+
- `InMemoryRepository<E, ID>`: Ready-to-use in-memory implementation.
|
|
100
|
+
|
|
101
|
+
### Decorators
|
|
102
|
+
- `@Repository(options)`: Registers the class as a singleton in `di-framework`.
|
|
103
|
+
|
|
104
|
+
### Types
|
|
105
|
+
- `StorageAdapter<E, ID>`: Interface for storage implementations.
|
|
106
|
+
- `Page<T>` / `PaginatedResult<T>`: Standardized pagination metadata.
|
|
107
|
+
- `EntityId`: Type alias for `string | number`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal protocol that every storage backend must implement.
|
|
3
|
+
* Keeps repository layer agnostic to the underlying technology.
|
|
4
|
+
*/
|
|
5
|
+
export interface StorageAdapter<E, ID = string | number> {
|
|
6
|
+
/**
|
|
7
|
+
* Retrieve one entity by ID
|
|
8
|
+
*/
|
|
9
|
+
findById(id: ID): Promise<E | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Retrieve multiple entities by IDs
|
|
12
|
+
*/
|
|
13
|
+
findMany(ids: ID[]): Promise<E[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve all entities (use with care – pagination usually preferred)
|
|
16
|
+
*/
|
|
17
|
+
findAll(): Promise<E[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Create or update (most implementations do upsert under the hood)
|
|
20
|
+
*/
|
|
21
|
+
save(entity: E): Promise<E>;
|
|
22
|
+
/**
|
|
23
|
+
* Delete by ID – returns whether deletion actually occurred
|
|
24
|
+
*/
|
|
25
|
+
delete(id: ID): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Optional: count matching records (used for pagination metadata)
|
|
28
|
+
*/
|
|
29
|
+
count(filter?: Record<string, any>): Promise<number>;
|
|
30
|
+
/**
|
|
31
|
+
* Optional: exists check (cheaper than findById in many backends)
|
|
32
|
+
*/
|
|
33
|
+
exists(id: ID): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Paginated listing with basic filtering/sorting support
|
|
36
|
+
* Filter/sort format is deliberately loose – concrete adapters interpret it.
|
|
37
|
+
*/
|
|
38
|
+
findPaginated(params: {
|
|
39
|
+
page?: number;
|
|
40
|
+
size?: number;
|
|
41
|
+
sort?: string | string[];
|
|
42
|
+
filter?: Record<string, any>;
|
|
43
|
+
withDeleted?: boolean;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
items: E[];
|
|
46
|
+
total: number;
|
|
47
|
+
page: number;
|
|
48
|
+
size: number;
|
|
49
|
+
pages: number;
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Optional – transaction support
|
|
53
|
+
* Return value of fn is returned from the transaction
|
|
54
|
+
*/
|
|
55
|
+
transaction<T>(fn: (adapter: this) => Promise<T>): Promise<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Optional – clean up / disconnect (important for tests, lambda, etc.)
|
|
58
|
+
*/
|
|
59
|
+
dispose?(): Promise<void> | void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Factory signature – allows dynamic creation of adapters
|
|
63
|
+
*/
|
|
64
|
+
export type StorageAdapterFactory<E, ID = string | number> = (config: unknown, // connection string, options, credentials, etc.
|
|
65
|
+
entityName?: string) => Promise<StorageAdapter<E, ID>> | StorageAdapter<E, ID>;
|
|
66
|
+
/**
|
|
67
|
+
* Helper type to extract Entity & ID from an adapter
|
|
68
|
+
*/
|
|
69
|
+
export type EntityOfAdapter<A> = A extends StorageAdapter<infer E, any> ? E : never;
|
|
70
|
+
export type IdOfAdapter<A> = A extends StorageAdapter<any, infer ID> ? ID : never;
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional decorator to register a repository with di-framework.
|
|
3
|
+
* If di-framework is not present, it will do nothing (but typically it will be present if this is used).
|
|
4
|
+
*
|
|
5
|
+
* @param options Registration options
|
|
6
|
+
*/
|
|
7
|
+
export declare function Repository(options?: {
|
|
8
|
+
singleton?: boolean;
|
|
9
|
+
container?: any;
|
|
10
|
+
}): (<T extends {
|
|
11
|
+
new (...args: any[]): {};
|
|
12
|
+
}>(constructor: T) => T) | ((target: any) => any);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Container } from '@di-framework/di-framework/decorators';
|
|
2
|
+
/**
|
|
3
|
+
* Optional decorator to register a repository with di-framework.
|
|
4
|
+
* If di-framework is not present, it will do nothing (but typically it will be present if this is used).
|
|
5
|
+
*
|
|
6
|
+
* @param options Registration options
|
|
7
|
+
*/
|
|
8
|
+
export function Repository(options = {}) {
|
|
9
|
+
try {
|
|
10
|
+
// We try to use di-framework's @Container decorator if it's available
|
|
11
|
+
return Container(options);
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
// If di-framework is not available or fails to load, we return a no-op decorator
|
|
15
|
+
return (target) => target;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EntityRepository } from './repository';
|
|
2
|
+
import type { EntityId, PaginatedResult } from './types';
|
|
3
|
+
export declare class InMemoryRepository<E extends {
|
|
4
|
+
id: ID;
|
|
5
|
+
}, ID extends string | number = EntityId> extends EntityRepository<E, ID> {
|
|
6
|
+
protected items: Map<ID, E>;
|
|
7
|
+
constructor();
|
|
8
|
+
findById(id: ID): Promise<E | null>;
|
|
9
|
+
findAll(): Promise<E[]>;
|
|
10
|
+
findMany(ids: ID[]): Promise<E[]>;
|
|
11
|
+
save(entity: E): Promise<E>;
|
|
12
|
+
delete(id: ID): Promise<boolean>;
|
|
13
|
+
upsert(entity: E): Promise<E>;
|
|
14
|
+
findPaginated(params: {
|
|
15
|
+
page?: number;
|
|
16
|
+
size?: number;
|
|
17
|
+
[p: string]: unknown;
|
|
18
|
+
}): Promise<PaginatedResult<E>>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EntityRepository } from './repository';
|
|
2
|
+
export class InMemoryRepository extends EntityRepository {
|
|
3
|
+
items = new Map();
|
|
4
|
+
constructor() {
|
|
5
|
+
super(undefined);
|
|
6
|
+
}
|
|
7
|
+
async findById(id) {
|
|
8
|
+
return this.items.get(this.normalizeId(id)) || null;
|
|
9
|
+
}
|
|
10
|
+
async findAll() {
|
|
11
|
+
return Array.from(this.items.values());
|
|
12
|
+
}
|
|
13
|
+
async findMany(ids) {
|
|
14
|
+
return ids
|
|
15
|
+
.map(id => this.items.get(this.normalizeId(id)))
|
|
16
|
+
.filter((e) => !!e);
|
|
17
|
+
}
|
|
18
|
+
async save(entity) {
|
|
19
|
+
this.items.set(this.normalizeId(entity.id), entity);
|
|
20
|
+
return entity;
|
|
21
|
+
}
|
|
22
|
+
async delete(id) {
|
|
23
|
+
return this.items.delete(this.normalizeId(id));
|
|
24
|
+
}
|
|
25
|
+
async upsert(entity) {
|
|
26
|
+
return this.save(entity);
|
|
27
|
+
}
|
|
28
|
+
async findPaginated(params) {
|
|
29
|
+
const page = params.page || 1;
|
|
30
|
+
const size = params.size || 10;
|
|
31
|
+
const all = await this.findAll();
|
|
32
|
+
// Filter out other params (simple exact match for this in-memory implementation)
|
|
33
|
+
const filters = Object.entries(params).filter(([key]) => key !== 'page' && key !== 'size');
|
|
34
|
+
const filtered = all.filter(item => {
|
|
35
|
+
return filters.every(([key, value]) => item[key] === value);
|
|
36
|
+
});
|
|
37
|
+
const start = (page - 1) * size;
|
|
38
|
+
const items = filtered.slice(start, start + size);
|
|
39
|
+
return {
|
|
40
|
+
items,
|
|
41
|
+
total: filtered.length,
|
|
42
|
+
page,
|
|
43
|
+
size,
|
|
44
|
+
pages: Math.ceil(filtered.length / size),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StorageAdapter } from './adapter';
|
|
2
|
+
import type { EntityId, PaginatedResult } from './types';
|
|
3
|
+
export declare abstract class BaseRepository<E, ID = EntityId> {
|
|
4
|
+
protected readonly adapter: StorageAdapter<E, ID>;
|
|
5
|
+
protected constructor(adapter: StorageAdapter<E, ID>);
|
|
6
|
+
protected normalizeId(id: ID): ID;
|
|
7
|
+
findById(id: ID): Promise<E | null>;
|
|
8
|
+
findAll(): Promise<E[]>;
|
|
9
|
+
findMany(ids: ID[]): Promise<E[]>;
|
|
10
|
+
save(entity: E): Promise<E>;
|
|
11
|
+
delete(id: ID): Promise<boolean>;
|
|
12
|
+
findPaginated(params: {
|
|
13
|
+
page?: number;
|
|
14
|
+
size?: number;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}): Promise<PaginatedResult<E>>;
|
|
17
|
+
protected inTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
18
|
+
dispose(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export type Database<T extends Record<string, typeof BaseRepository>> = {
|
|
21
|
+
[K in keyof T]: T[K] extends new (...args: any[]) => infer R ? R : T[K] extends {
|
|
22
|
+
prototype: infer P;
|
|
23
|
+
} ? P : never;
|
|
24
|
+
};
|
|
25
|
+
export type RepositoryMap<T> = {
|
|
26
|
+
[K in keyof T]: T[K] extends BaseRepository<infer E> ? BaseRepository<E> : never;
|
|
27
|
+
};
|
|
28
|
+
export declare abstract class EntityRepository<E, ID = EntityId> extends BaseRepository<E, ID> {
|
|
29
|
+
}
|
|
30
|
+
export type EntityOf<R> = R extends BaseRepository<infer E> ? E : never;
|
|
31
|
+
export type IdOf<R> = R extends BaseRepository<any, infer ID> ? ID : never;
|
|
32
|
+
export interface SoftDeletable {
|
|
33
|
+
deletedAt: Date | null;
|
|
34
|
+
}
|
|
35
|
+
export declare abstract class SoftDeleteRepository<E extends SoftDeletable & {
|
|
36
|
+
id: ID;
|
|
37
|
+
}, ID = EntityId> extends EntityRepository<E, ID> {
|
|
38
|
+
abstract softDelete(id: ID): Promise<boolean>;
|
|
39
|
+
abstract restore(id: ID): Promise<boolean>;
|
|
40
|
+
abstract findActive(): Promise<E[]>;
|
|
41
|
+
abstract findDeleted(): Promise<E[]>;
|
|
42
|
+
protected abstract afterNotifyDelete(id: ID): void;
|
|
43
|
+
softDeleteAndNotify(id: ID): Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
export interface SearchableRepository<E, ID = EntityId> {
|
|
46
|
+
search(query: string, params?: {
|
|
47
|
+
page?: number;
|
|
48
|
+
size?: number;
|
|
49
|
+
}): Promise<PaginatedResult<E>>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class BaseRepository {
|
|
2
|
+
adapter;
|
|
3
|
+
constructor(adapter) {
|
|
4
|
+
this.adapter = adapter;
|
|
5
|
+
}
|
|
6
|
+
normalizeId(id) {
|
|
7
|
+
// Default normalization (stringify) – can be overridden by subclasses if needed
|
|
8
|
+
return typeof id === 'string' ? id : String(id);
|
|
9
|
+
}
|
|
10
|
+
// Forward most calls directly (you can add caching, validation, events here)
|
|
11
|
+
async findById(id) {
|
|
12
|
+
return this.adapter.findById(this.normalizeId(id));
|
|
13
|
+
}
|
|
14
|
+
async findAll() {
|
|
15
|
+
return this.adapter.findAll();
|
|
16
|
+
}
|
|
17
|
+
async findMany(ids) {
|
|
18
|
+
return this.adapter.findMany(ids.map(id => this.normalizeId(id)));
|
|
19
|
+
}
|
|
20
|
+
async save(entity) {
|
|
21
|
+
return this.adapter.save(entity);
|
|
22
|
+
}
|
|
23
|
+
async delete(id) {
|
|
24
|
+
return this.adapter.delete(this.normalizeId(id));
|
|
25
|
+
}
|
|
26
|
+
async findPaginated(params) {
|
|
27
|
+
return this.adapter.findPaginated(params);
|
|
28
|
+
}
|
|
29
|
+
// Transaction support (if adapter provides it)
|
|
30
|
+
async inTransaction(fn) {
|
|
31
|
+
if (typeof this.adapter.transaction === 'function') {
|
|
32
|
+
return this.adapter.transaction(() => fn());
|
|
33
|
+
}
|
|
34
|
+
return fn(); // fallback
|
|
35
|
+
}
|
|
36
|
+
async dispose() {
|
|
37
|
+
if (typeof this.adapter.dispose === 'function') {
|
|
38
|
+
await this.adapter.dispose();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class EntityRepository extends BaseRepository {
|
|
43
|
+
}
|
|
44
|
+
export class SoftDeleteRepository extends EntityRepository {
|
|
45
|
+
async softDeleteAndNotify(id) {
|
|
46
|
+
const ok = await this.softDelete(id);
|
|
47
|
+
if (ok)
|
|
48
|
+
this.afterNotifyDelete(id);
|
|
49
|
+
return ok;
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@di-framework/di-framework-repo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A coherent abstraction of repositories and storage adapters for di-framework.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@di-framework/di-framework": "2.0.4",
|
|
18
|
+
"typescript": "^5"
|
|
19
|
+
},
|
|
20
|
+
"peerDependenciesMeta": {
|
|
21
|
+
"@di-framework/di-framework": {
|
|
22
|
+
"optional": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md"
|
|
28
|
+
]
|
|
29
|
+
}
|