@di-framework/di-framework-repo 0.0.0-prerelease-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,128 @@
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
+ Required for DI integration: If you want to use the `@Repository` decorator for dependency injection, install the DI framework peer dependency.
20
+
21
+ ```bash
22
+ bun add @di-framework/di-framework
23
+ ```
24
+
25
+ Important: Always import from the scoped package name `@di-framework/di-framework/*`.
26
+
27
+ Mixing different import IDs (e.g., `di-framework/*` or relative paths to sources) can load a second copy of the library and create a second global container instance.
28
+
29
+ Correct:
30
+
31
+ ```ts
32
+ import { useContainer } from "@di-framework/di-framework/container";
33
+ import { Container, Component } from "@di-framework/di-framework/decorators";
34
+ ```
35
+
36
+ Avoid:
37
+
38
+ ```ts
39
+ import { useContainer } from "di-framework/container"; // Wrong: unscoped id
40
+ import { Container } from "../../di-framework/decorators"; // Wrong: relative id
41
+ ```
42
+
43
+ ## Basic Usage
44
+
45
+ ### 1. Define your Entity
46
+
47
+ ```typescript
48
+ interface User {
49
+ id: number;
50
+ name: string;
51
+ email: string;
52
+ }
53
+ ```
54
+
55
+ ### 2. Implement a Repository
56
+
57
+ You can extend `InMemoryRepository` for quick prototyping:
58
+
59
+ ```typescript
60
+ import { InMemoryRepository } from "@geoffsee/df-repo";
61
+
62
+ class UserRepository extends InMemoryRepository<User, number> {
63
+ async findByEmail(email: string): Promise<User | null> {
64
+ const all = await this.findAll();
65
+ return all.find((u) => u.email === email) || null;
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### 3. Use with di-framework
71
+
72
+ Use the `@Repository` decorator to automatically register your repository with the `di-framework` container.
73
+
74
+ ```typescript
75
+ import { Repository } from "@geoffsee/df-repo";
76
+
77
+ @Repository()
78
+ class UserRepository extends InMemoryRepository<User, number> {
79
+ // ...
80
+ }
81
+
82
+ // In another service
83
+ @Container()
84
+ class UserService {
85
+ constructor(@Component(UserRepository) private users: UserRepository) {}
86
+
87
+ async listUsers() {
88
+ return this.users.findAll();
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Storage Adapters
94
+
95
+ The `StorageAdapter` interface allows you to implement custom backends.
96
+
97
+ ```typescript
98
+ import { StorageAdapter, BaseRepository } from "@geoffsee/df-repo";
99
+
100
+ class MyCustomAdapter<E, ID> implements StorageAdapter<E, ID> {
101
+ // Implement findById, save, delete, findPaginated, etc.
102
+ }
103
+
104
+ class MyRepository extends BaseRepository<User, number> {
105
+ constructor(adapter: MyCustomAdapter<User, number>) {
106
+ super(adapter);
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## API Overview
112
+
113
+ ### Repository Classes
114
+
115
+ - `BaseRepository<E, ID>`: The foundational repository class.
116
+ - `EntityRepository<E, ID>`: Standard entity-aware repository.
117
+ - `SoftDeleteRepository<E, ID>`: Repository with soft-delete capabilities.
118
+ - `InMemoryRepository<E, ID>`: Ready-to-use in-memory implementation.
119
+
120
+ ### Decorators
121
+
122
+ - `@Repository(options)`: Registers the class as a singleton in `di-framework`.
123
+
124
+ ### Types
125
+
126
+ - `StorageAdapter<E, ID>`: Interface for storage implementations.
127
+ - `Page<T>` / `PaginatedResult<T>`: Standardized pagination metadata.
128
+ - `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,15 @@
1
+ /**
2
+ * Repository decorator.
3
+ *
4
+ * This package requires `@di-framework/di-framework` as a peer dependency and
5
+ * delegates to its `@Container` decorator to register repositories with the
6
+ * same singleton/global container instance. Ensure you always import the DI
7
+ * framework using the scoped package name (`@di-framework/di-framework/*`) to
8
+ * avoid loading multiple copies and accidentally creating multiple containers.
9
+ */
10
+ export declare function Repository(options?: {
11
+ singleton?: boolean;
12
+ container?: any;
13
+ }): <T extends {
14
+ new (...args: any[]): {};
15
+ }>(constructor: T) => T;
@@ -0,0 +1,13 @@
1
+ import { Container as ContainerDecorator } from "@di-framework/di-framework/decorators";
2
+ /**
3
+ * Repository decorator.
4
+ *
5
+ * This package requires `@di-framework/di-framework` as a peer dependency and
6
+ * delegates to its `@Container` decorator to register repositories with the
7
+ * same singleton/global container instance. Ensure you always import the DI
8
+ * framework using the scoped package name (`@di-framework/di-framework/*`) to
9
+ * avoid loading multiple copies and accidentally creating multiple containers.
10
+ */
11
+ export function Repository(options = {}) {
12
+ return ContainerDecorator(options);
13
+ }
@@ -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,24 @@
1
+ {
2
+ "name": "@di-framework/di-framework-repo",
3
+ "version": "0.0.0-prerelease-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
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ]
24
+ }