@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 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;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './adapter';
3
+ export * from './repository';
4
+ export * from './in-memory';
5
+ export * from './decorators';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './adapter';
3
+ export * from './repository';
4
+ export * from './in-memory';
5
+ export * from './decorators';
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export type EntityId = string | number;
2
+ export interface Page<T> {
3
+ items: T[];
4
+ total: number;
5
+ page: number;
6
+ size: number;
7
+ pages: number;
8
+ }
9
+ export type PaginatedResult<T> = Page<T>;
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
+ }