entity-repository 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kurt Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,390 @@
1
+ # entity-repository
2
+
3
+ Type-safe entity caching and state management with RxJS observables and React integration.
4
+
5
+ ## Features
6
+
7
+ - **Type-safe entity management** - Full TypeScript generics for entity definitions and ID types
8
+ - **In-memory caching** - Fast Map-based cache by table and entity ID
9
+ - **RxJS-based reactivity** - Observable streams for real-time updates
10
+ - **Query layer** - `RecordQuery` for single entities, `ListQuery` for filtered/sorted lists
11
+ - **React integration** - Context provider and hooks for seamless React usage
12
+ - **Request deduplication** - Prevents duplicate fetch requests for the same entity
13
+ - **Real-time event system** - Insert/update/delete events for reactive list updates
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install entity-repository rxjs
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Define your entity types
24
+
25
+ ```typescript
26
+ import type { EntityConfig } from "entity-repository";
27
+
28
+ // Define the shape of your entities
29
+ type Entities = {
30
+ users: { id: string; name: string; email: string };
31
+ posts: { id: string; title: string; authorId: string; createdAt: string };
32
+ };
33
+
34
+ // Configure which field is the ID for each entity
35
+ const entityConfig = {
36
+ users: { id: "id" },
37
+ posts: { id: "id" },
38
+ } as const satisfies EntityConfig<Entities>;
39
+
40
+ type MyEntityConfig = typeof entityConfig;
41
+ ```
42
+
43
+ ### 2. Create the repository
44
+
45
+ ```typescript
46
+ import { Repository } from "entity-repository";
47
+
48
+ const repository = new Repository<Entities, MyEntityConfig>({
49
+ entities: entityConfig,
50
+ });
51
+ ```
52
+
53
+ ### 3. Basic operations
54
+
55
+ ```typescript
56
+ // Store an entity
57
+ repository.set("users", { id: "1", name: "Alice", email: "alice@example.com" });
58
+
59
+ // Get an entity (returns null if not cached)
60
+ const user = repository.get("users", { id: "1" });
61
+
62
+ // Fetch with caching (fetches only if not cached, deduplicates concurrent requests)
63
+ const user = await repository.fetch("users", { id: "1" }, async (id) => {
64
+ const response = await fetch(`/api/users/${id.id}`);
65
+ return response.json();
66
+ });
67
+
68
+ // Delete an entity
69
+ repository.del("users", { id: "1" });
70
+ ```
71
+
72
+ ### 4. Reactive subscriptions
73
+
74
+ ```typescript
75
+ // Subscribe to entity changes
76
+ const observable = repository.getObservable("users", { id: "1" });
77
+ observable.subscribe((user) => {
78
+ console.log("User changed:", user);
79
+ });
80
+
81
+ // Subscribe to all events for a table (insert/update/delete)
82
+ repository.getEvents("users").subscribe((event) => {
83
+ if (event.type === "insert") console.log("New user:", event.new);
84
+ if (event.type === "update") console.log("Updated:", event.old, "->", event.new);
85
+ if (event.type === "delete") console.log("Deleted:", event.old);
86
+ });
87
+ ```
88
+
89
+ ## React Integration
90
+
91
+ ### Setup
92
+
93
+ ```typescript
94
+ import { useMemo } from "react";
95
+ import { createRepositoryContext, Repository } from "entity-repository";
96
+
97
+ // Create typed context and hooks
98
+ export const {
99
+ RepositoryProvider,
100
+ useRepository,
101
+ useRepositoryQuery,
102
+ useRepositoryListQuery,
103
+ useSubscribedState,
104
+ } = createRepositoryContext<Entities, MyEntityConfig>();
105
+
106
+ // In your app root
107
+ function App() {
108
+ const repository = useMemo(
109
+ () => new Repository<Entities, MyEntityConfig>({ entities: entityConfig }),
110
+ []
111
+ );
112
+
113
+ return (
114
+ <RepositoryProvider repository={repository}>
115
+ <YourApp />
116
+ </RepositoryProvider>
117
+ );
118
+ }
119
+ ```
120
+
121
+ ### useRepositoryQuery - Single entity
122
+
123
+ ```typescript
124
+ function UserProfile({ userId }: { userId: string }) {
125
+ const { entity: user, status } = useRepositoryQuery(
126
+ "users",
127
+ { id: userId },
128
+ async (id) => {
129
+ const response = await fetch(`/api/users/${id.id}`);
130
+ return response.json();
131
+ }
132
+ );
133
+
134
+ if (status === "fetching") return <div>Loading...</div>;
135
+ if (status === "error") return <div>Error loading user</div>;
136
+ if (!user) return <div>User not found</div>;
137
+
138
+ return <div>{user.name}</div>;
139
+ }
140
+ ```
141
+
142
+ ### useRepositoryListQuery - Entity lists with filter/sort
143
+
144
+ ```typescript
145
+ function UserPosts({ authorId }: { authorId: string }) {
146
+ const { records: posts, status } = useRepositoryListQuery(
147
+ "posts",
148
+ { authorId }, // param - query re-runs when this changes
149
+ {
150
+ filter: (post) => post.authorId === authorId,
151
+ order: (a, b) => b.createdAt.localeCompare(a.createdAt),
152
+ },
153
+ async (param) => {
154
+ const response = await fetch(`/api/posts?author=${param.authorId}`);
155
+ return response.json();
156
+ }
157
+ );
158
+
159
+ if (status === "fetching" && posts.length === 0) return <div>Loading...</div>;
160
+
161
+ return (
162
+ <ul>
163
+ {posts.map((post) => (
164
+ <li key={post.id}>{post.title}</li>
165
+ ))}
166
+ </ul>
167
+ );
168
+ }
169
+ ```
170
+
171
+ ### useSubscribedState - Subscribe to any RxJS observable
172
+
173
+ ```typescript
174
+ import { interval } from "rxjs";
175
+
176
+ function Timer() {
177
+ const seconds = useSubscribedState(interval(1000), 0);
178
+ return <div>Seconds: {seconds}</div>;
179
+ }
180
+ ```
181
+
182
+ ## Real-time Updates Integration
183
+
184
+ The repository emits events when entities change. Connect to your real-time backend:
185
+
186
+ ```typescript
187
+ // Example with Supabase Realtime
188
+ useEffect(() => {
189
+ const channel = supabase
190
+ .channel("changes")
191
+ .on("postgres_changes", { event: "*", schema: "public" }, (payload) => {
192
+ const table = payload.table as keyof Entities;
193
+
194
+ if (payload.eventType === "DELETE") {
195
+ repository.del(table, { id: payload.old.id });
196
+ } else {
197
+ repository.set(table, payload.new);
198
+ }
199
+ })
200
+ .subscribe();
201
+
202
+ return () => supabase.removeChannel(channel);
203
+ }, [repository, supabase]);
204
+ ```
205
+
206
+ ## Advanced Usage
207
+
208
+ ### RecordQuery - Standalone single-entity query
209
+
210
+ ```typescript
211
+ const query = repository.recordQuery("users", { id: "1" }, async (id) => {
212
+ const response = await fetch(`/api/users/${id.id}`);
213
+ return response.json();
214
+ });
215
+
216
+ // Subscribe to state changes
217
+ query.$state.subscribe((state) => {
218
+ console.log("Status:", state.status);
219
+ console.log("Entity:", state.entity);
220
+ });
221
+
222
+ // Manually trigger a fetch
223
+ await query.fetch();
224
+
225
+ // Cleanup when done
226
+ query.dispose();
227
+ ```
228
+
229
+ ### ListQuery - Standalone list query
230
+
231
+ ```typescript
232
+ const query = repository.listQuery(
233
+ "posts",
234
+ {
235
+ filter: (post) => post.authorId === "1",
236
+ order: (a, b) => b.createdAt.localeCompare(a.createdAt),
237
+ },
238
+ async () => {
239
+ const response = await fetch("/api/posts?author=1");
240
+ return response.json();
241
+ }
242
+ );
243
+
244
+ // Subscribe to records and status separately
245
+ query.$records.subscribe((posts) => {
246
+ console.log("Posts:", posts);
247
+ });
248
+
249
+ query.$status.subscribe((status) => {
250
+ console.log("Status:", status.status);
251
+ });
252
+
253
+ // Manually trigger a refetch
254
+ await query.refetch();
255
+
256
+ // Cleanup when done
257
+ query.dispose();
258
+ ```
259
+
260
+ ### Cache key utilities
261
+
262
+ ```typescript
263
+ // Get the cache key for an entity ID
264
+ const key = repository.getCacheKey("users", { id: "1" }); // "1"
265
+
266
+ // Get the cache key from an entity object
267
+ const key = repository.getEntityKey("users", { id: "1", name: "Alice", email: "..." }); // "1"
268
+ ```
269
+
270
+ ## API Reference
271
+
272
+ ### Repository
273
+
274
+ | Method | Description |
275
+ |--------|-------------|
276
+ | `set(table, entity)` | Store entity in cache, emits insert/update event |
277
+ | `get(table, id)` | Get cached entity or null |
278
+ | `del(table, id)` | Remove entity from cache, emits delete event |
279
+ | `fetch(table, id, fetcher)` | Get cached or fetch, deduplicates concurrent requests |
280
+ | `getObservable(table, id)` | BehaviorSubject for entity changes |
281
+ | `getEvents(table)` | Subject emitting insert/update/delete events |
282
+ | `recordQuery(table, id, fetcher)` | Create RecordQuery instance |
283
+ | `listQuery(table, options, fetcher)` | Create ListQuery instance |
284
+ | `getCacheKey(table, id)` | Get cache key string from entity ID |
285
+ | `getEntityKey(table, entity)` | Get cache key string from entity object |
286
+
287
+ ### RecordQuery
288
+
289
+ Manages single-entity queries with status tracking.
290
+
291
+ | Property/Method | Description |
292
+ |-----------------|-------------|
293
+ | `$state` | `BehaviorSubject<{ entity, status }>` - subscribe to state changes |
294
+ | `fetch()` | Manually trigger fetch, returns entity |
295
+ | `dispose()` | Cleanup subscriptions |
296
+
297
+ ### ListQuery
298
+
299
+ Manages list queries with filter/sort and real-time updates.
300
+
301
+ | Property/Method | Description |
302
+ |-----------------|-------------|
303
+ | `$records` | `BehaviorSubject<Entity[]>` - subscribe to record list |
304
+ | `$status` | `BehaviorSubject<{ status, error? }>` - subscribe to status |
305
+ | `refetch()` | Manually trigger refetch, returns records |
306
+ | `dispose()` | Cleanup subscriptions |
307
+
308
+ ### React Hooks
309
+
310
+ | Hook | Description |
311
+ |------|-------------|
312
+ | `useRepository()` | Access repository instance from context |
313
+ | `useRepositoryQuery(table, id, fetcher)` | Subscribe to single entity query |
314
+ | `useRepositoryListQuery(table, param, options, fetcher)` | Subscribe to filtered/sorted list query |
315
+ | `useSubscribedState(observable, initial)` | Subscribe to any RxJS observable |
316
+
317
+ ## Types
318
+
319
+ ```typescript
320
+ // Entity configuration - maps tables to their ID field
321
+ type EntityConfig<Definitions> = {
322
+ [Table in keyof Definitions]: { id: keyof Definitions[Table] & string };
323
+ };
324
+
325
+ // Entity ID tuple - picks only the ID field from entity
326
+ type EntityIdTuple<Definitions, Config, Table> =
327
+ Pick<Definitions[Table], Config[Table]["id"]>;
328
+
329
+ // Entity event types - emitted on insert/update/delete
330
+ type EntityEvent<Entity> = {
331
+ timestamp: Date;
332
+ } & (
333
+ | { type: "insert"; new: Entity }
334
+ | { type: "update"; old: Entity; new: Entity }
335
+ | { type: "delete"; old: Entity }
336
+ );
337
+
338
+ // Single-entity query state
339
+ type RepositoryQuery<Entity> = {
340
+ entity: Entity | null;
341
+ } & (
342
+ | { status: "fetching" }
343
+ | { status: "idle" }
344
+ | { status: "error"; error: Error }
345
+ );
346
+
347
+ // List query options
348
+ type ListQueryOptions<Entity> = {
349
+ filter?: (entity: Entity) => boolean;
350
+ order?: (left: Entity, right: Entity) => number;
351
+ };
352
+
353
+ // List query status
354
+ type ListQueryStatus =
355
+ | { status: "idle" }
356
+ | { status: "fetching" }
357
+ | { status: "error"; error: Error };
358
+
359
+ // List query state (returned by useRepositoryListQuery)
360
+ type ListQueryState<Entity> = {
361
+ records: Entity[];
362
+ } & ListQueryStatus;
363
+ ```
364
+
365
+ ## How It Works
366
+
367
+ ### Caching Strategy
368
+
369
+ The repository uses a simple Map-based cache keyed by table name and entity ID:
370
+
371
+ 1. **Set**: Stores entity and emits insert (new) or update (existing) event
372
+ 2. **Get**: Returns cached entity or null (synchronous)
373
+ 3. **Fetch**: Returns cached entity, or fetches and caches if not present
374
+ 4. **Request deduplication**: Concurrent fetches for the same entity share a single request
375
+
376
+ ### Reactivity
377
+
378
+ - `getObservable()` returns a `BehaviorSubject` that emits when an entity changes
379
+ - `getEvents()` returns a `Subject` that emits insert/update/delete events for a table
380
+ - `ListQuery` subscribes to table events and automatically updates its filtered list
381
+
382
+ ### React Integration
383
+
384
+ - `useRepositoryQuery` creates a `RecordQuery` and subscribes to its state
385
+ - `useRepositoryListQuery` creates a `ListQuery` and subscribes to records + status
386
+ - Both hooks use `JSON.stringify(id/param)` for stable memoization keys
387
+
388
+ ## License
389
+
390
+ MIT
@@ -0,0 +1,6 @@
1
+ export * from "./types";
2
+ export * from "./record-query";
3
+ export * from "./list-query";
4
+ export * from "./repository";
5
+ export * from "./react";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./record-query";
3
+ export * from "./list-query";
4
+ export * from "./repository";
5
+ export * from "./react";
@@ -0,0 +1,36 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ import type { EntityConfig, EntityDefinitions } from "./types";
3
+ import type { Repository } from "./repository";
4
+ export type ListQueryOptions<Entity> = {
5
+ order?: (left: Entity, right: Entity) => number;
6
+ filter?: (entity: Entity) => boolean;
7
+ };
8
+ export type ListQueryStatus = {
9
+ status: "idle";
10
+ } | {
11
+ status: "fetching";
12
+ } | {
13
+ status: "error";
14
+ error: Error;
15
+ };
16
+ export type ListQueryState<Entity> = {
17
+ records: Entity[];
18
+ } & ListQueryStatus;
19
+ export declare class ListQuery<Definitions extends EntityDefinitions, Config extends EntityConfig<Definitions>, Table extends keyof Definitions> {
20
+ readonly $records: BehaviorSubject<Definitions[Table][]>;
21
+ readonly $status: BehaviorSubject<ListQueryStatus>;
22
+ private repository;
23
+ private table;
24
+ private fetcher;
25
+ private filter;
26
+ private order;
27
+ private subscription;
28
+ constructor(repository: Repository<Definitions, Config>, table: Table, options: ListQueryOptions<Definitions[Table]>, fetcher: () => Promise<Definitions[Table][]>);
29
+ refetch(): Promise<Definitions[Table][]>;
30
+ dispose(): void;
31
+ private applyEvent;
32
+ private upsertRecord;
33
+ private removeRecord;
34
+ private applyOrdering;
35
+ }
36
+ //# sourceMappingURL=list-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-query.d.ts","sourceRoot":"","sources":["../src/list-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgB,MAAM,MAAM,CAAC;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAe,MAAM,SAAS,CAAC;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,MAAM,gBAAgB,CAAC,MAAM,IAAI;IACrC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,GACtB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEtC,MAAM,MAAM,cAAc,CAAC,MAAM,IAAI;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,GAAG,eAAe,CAAC;AAEpB,qBAAa,SAAS,CACpB,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,EACxC,KAAK,SAAS,MAAM,WAAW;IAE/B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACzD,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC,eAAe,CAAC,CAAC;IACnD,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,KAAK,CAA2E;IACxF,OAAO,CAAC,YAAY,CAAe;gBAGjC,UAAU,EAAE,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,EAC3C,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAC7C,OAAO,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;IAkBxC,OAAO;IAmBb,OAAO;IAIP,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,YAAY;IAyCpB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,aAAa;CAOtB"}
@@ -0,0 +1,109 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ export class ListQuery {
3
+ $records;
4
+ $status;
5
+ repository;
6
+ table;
7
+ fetcher;
8
+ filter;
9
+ order;
10
+ subscription;
11
+ constructor(repository, table, options, fetcher) {
12
+ this.repository = repository;
13
+ this.table = table;
14
+ this.fetcher = fetcher;
15
+ this.filter = options.filter ?? (() => true);
16
+ this.order = options.order ?? null;
17
+ this.$records = new BehaviorSubject([]);
18
+ this.$status = new BehaviorSubject({ status: "fetching" });
19
+ this.subscription = repository.getEvents(table).subscribe((event) => {
20
+ this.applyEvent(event);
21
+ });
22
+ void this.refetch();
23
+ }
24
+ async refetch() {
25
+ this.$status.next({ status: "fetching" });
26
+ try {
27
+ const records = await this.fetcher();
28
+ records.forEach((record) => {
29
+ this.repository.set(this.table, record);
30
+ });
31
+ const nextRecords = this.applyOrdering(records.filter(this.filter));
32
+ this.$records.next(nextRecords);
33
+ this.$status.next({ status: "idle" });
34
+ return nextRecords;
35
+ }
36
+ catch (error) {
37
+ const normalizedError = error instanceof Error ? error : new Error("ListQuery fetch failed");
38
+ this.$status.next({ status: "error", error: normalizedError });
39
+ throw normalizedError;
40
+ }
41
+ }
42
+ dispose() {
43
+ this.subscription.unsubscribe();
44
+ }
45
+ applyEvent(event) {
46
+ const current = this.$records.value;
47
+ switch (event.type) {
48
+ case "insert":
49
+ this.upsertRecord(current, event.new);
50
+ return;
51
+ case "update":
52
+ this.upsertRecord(current, event.new, event.old);
53
+ return;
54
+ case "delete":
55
+ this.removeRecord(current, event.old);
56
+ return;
57
+ default: {
58
+ const _exhaustive = event;
59
+ return _exhaustive;
60
+ }
61
+ }
62
+ }
63
+ upsertRecord(current, nextRecord, previousRecord) {
64
+ const nextPasses = this.filter(nextRecord);
65
+ const nextKey = this.repository.getEntityKey(this.table, nextRecord);
66
+ const previousKey = previousRecord
67
+ ? this.repository.getEntityKey(this.table, previousRecord)
68
+ : null;
69
+ const index = current.findIndex((record) => this.repository.getEntityKey(this.table, record) === nextKey);
70
+ if (!nextPasses) {
71
+ if (index !== -1) {
72
+ const next = current.slice();
73
+ next.splice(index, 1);
74
+ this.$records.next(this.applyOrdering(next));
75
+ }
76
+ return;
77
+ }
78
+ const next = current.slice();
79
+ if (index === -1) {
80
+ next.push(nextRecord);
81
+ }
82
+ else {
83
+ next[index] = nextRecord;
84
+ }
85
+ if (previousKey && previousKey !== nextKey) {
86
+ const previousIndex = next.findIndex((record, recordIndex) => recordIndex !== index && this.repository.getEntityKey(this.table, record) === previousKey);
87
+ if (previousIndex !== -1) {
88
+ next.splice(previousIndex, 1);
89
+ }
90
+ }
91
+ this.$records.next(this.applyOrdering(next));
92
+ }
93
+ removeRecord(current, record) {
94
+ const key = this.repository.getEntityKey(this.table, record);
95
+ const index = current.findIndex((existing) => this.repository.getEntityKey(this.table, existing) === key);
96
+ if (index === -1) {
97
+ return;
98
+ }
99
+ const next = current.slice();
100
+ next.splice(index, 1);
101
+ this.$records.next(this.applyOrdering(next));
102
+ }
103
+ applyOrdering(records) {
104
+ if (!this.order) {
105
+ return records;
106
+ }
107
+ return [...records].sort(this.order);
108
+ }
109
+ }
@@ -0,0 +1,16 @@
1
+ import { ReactNode } from "react";
2
+ import { type Observable } from "rxjs";
3
+ import type { ListQueryOptions, ListQueryState } from "./list-query";
4
+ import type { EntityConfig, EntityDefinitions, EntityIdTuple, RepositoryQuery } from "./types";
5
+ import { Repository } from "./repository";
6
+ export declare function createRepositoryContext<Definitions extends EntityDefinitions, Config extends EntityConfig<Definitions> = EntityConfig<Definitions>>(): {
7
+ RepositoryProvider: ({ repository, children, }: {
8
+ repository: Repository<Definitions, Config>;
9
+ children: ReactNode;
10
+ }) => import("react/jsx-runtime").JSX.Element;
11
+ useRepository: () => Repository<Definitions, Config>;
12
+ useRepositoryQuery: <Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>, fetcher: (id: EntityIdTuple<Definitions, Config, Table>) => Promise<Definitions[Table]>) => RepositoryQuery<Definitions[Table]>;
13
+ useRepositoryListQuery: <Table extends keyof Definitions, Param>(table: Table, param: Param, options: ListQueryOptions<Definitions[Table]>, fetcher: (param: Param) => Promise<Definitions[Table][]>) => ListQueryState<Definitions[Table]>;
14
+ useSubscribedState: <Value>(observable: Observable<Value>, initialValue: Value) => Value;
15
+ };
16
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,SAAS,EAA4C,MAAM,OAAO,CAAC;AAE3F,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/F,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,wBAAgB,uBAAuB,CACrC,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,GAAG,YAAY,CAAC,WAAW,CAAC;oDAOjE;QACD,UAAU,EAAE,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC5C,QAAQ,EAAE,SAAS,CAAC;KACrB;;yBAoC2B,KAAK,SAAS,MAAM,WAAW,SAClD,KAAK,MACR,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,WACpC,CAAC,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,KACtF,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;6BAiBN,KAAK,SAAS,MAAM,WAAW,EAAE,KAAK,SAC7D,KAAK,SACL,KAAK,WACH,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,WACpC,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,KACvD,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;yBA9CT,KAAK,cAAc,UAAU,CAAC,KAAK,CAAC,gBAAgB,KAAK;EA2EtF"}
package/dist/react.js ADDED
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
3
+ import { combineLatest, map } from "rxjs";
4
+ export function createRepositoryContext() {
5
+ const RepositoryReactContext = createContext(null);
6
+ function RepositoryProvider({ repository, children, }) {
7
+ return (_jsx(RepositoryReactContext.Provider, { value: repository, children: children }));
8
+ }
9
+ function useRepository() {
10
+ const context = useContext(RepositoryReactContext);
11
+ if (!context) {
12
+ throw new Error("RepositoryProvider is missing.");
13
+ }
14
+ return context;
15
+ }
16
+ function useSubscribedState(observable, initialValue) {
17
+ const [state, setState] = useState(initialValue);
18
+ useEffect(() => {
19
+ const subscription = observable.subscribe((value) => {
20
+ setState(value);
21
+ });
22
+ return () => subscription.unsubscribe();
23
+ }, [observable]);
24
+ return state;
25
+ }
26
+ /**
27
+ * Subscribes to a single-record query keyed only by `id`.
28
+ *
29
+ * NOTE: `table` and `fetcher` changes are intentionally ignored so the
30
+ * query instance remains stable for a given `id`.
31
+ */
32
+ function useRepositoryQuery(table, id, fetcher) {
33
+ const repository = useRepository();
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- table/fetcher intentionally ignored
35
+ const recordQuery = useMemo(() => repository.recordQuery(table, id, fetcher), [repository, JSON.stringify(id)]);
36
+ return useSubscribedState(recordQuery.$state, recordQuery.$state.value);
37
+ }
38
+ /**
39
+ * Subscribes to a list query keyed only by `param`.
40
+ *
41
+ * NOTE: `options` and `fetcher` changes are intentionally ignored so the
42
+ * list query instance remains stable for a given `param`.
43
+ */
44
+ function useRepositoryListQuery(table, param, options, fetcher) {
45
+ const repository = useRepository();
46
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- table/options/fetcher intentionally ignored
47
+ const listQuery = useMemo(() => repository.listQuery(table, options, () => fetcher(param)), [repository, JSON.stringify(param)]);
48
+ useEffect(() => {
49
+ return () => listQuery.dispose();
50
+ }, [listQuery]);
51
+ const $state = useMemo(() => combineLatest([listQuery.$records, listQuery.$status]).pipe(map(([records, status]) => ({ records, ...status }))), [listQuery]);
52
+ return useSubscribedState($state, { records: listQuery.$records.value, ...listQuery.$status.value });
53
+ }
54
+ return {
55
+ RepositoryProvider,
56
+ useRepository,
57
+ useRepositoryQuery,
58
+ useRepositoryListQuery,
59
+ useSubscribedState,
60
+ };
61
+ }
@@ -0,0 +1,15 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ import type { EntityConfig, EntityDefinitions, EntityIdTuple, RepositoryQuery } from "./types";
3
+ import type { Repository } from "./repository";
4
+ export declare class RecordQuery<Definitions extends EntityDefinitions, Config extends EntityConfig<Definitions>, Table extends keyof Definitions> {
5
+ readonly $state: BehaviorSubject<RepositoryQuery<Definitions[Table]>>;
6
+ private subscription;
7
+ private repository;
8
+ private table;
9
+ private id;
10
+ private fetcher;
11
+ constructor(repository: Repository<Definitions, Config>, table: Table, id: EntityIdTuple<Definitions, Config, Table>, fetcher: (id: EntityIdTuple<Definitions, Config, Table>) => Promise<Definitions[Table]>);
12
+ fetch(): Promise<Definitions[Table]>;
13
+ dispose(): void;
14
+ }
15
+ //# sourceMappingURL=record-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-query.d.ts","sourceRoot":"","sources":["../src/record-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAgB,MAAM,MAAM,CAAC;AAErD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,eAAe,EAChB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,qBAAa,WAAW,CACtB,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,EACxC,KAAK,SAAS,MAAM,WAAW;IAE/B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,EAAE,CAA4C;IACtD,OAAO,CAAC,OAAO,CAAiF;gBAG9F,UAAU,EAAE,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,EAC3C,KAAK,EAAE,KAAK,EACZ,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAC7C,OAAO,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAuBnF,KAAK;IA2BX,OAAO;CAGR"}
@@ -0,0 +1,55 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ export class RecordQuery {
3
+ $state;
4
+ subscription;
5
+ repository;
6
+ table;
7
+ id;
8
+ fetcher;
9
+ constructor(repository, table, id, fetcher) {
10
+ this.repository = repository;
11
+ this.table = table;
12
+ this.id = id;
13
+ this.fetcher = fetcher;
14
+ const entity = repository.get(table, id);
15
+ const initialState = entity
16
+ ? { entity, status: "idle" }
17
+ : { entity: null, status: "fetching" };
18
+ this.$state = new BehaviorSubject(initialState);
19
+ this.subscription = repository.getObservable(table, id).subscribe((value) => {
20
+ this.$state.next({ ...this.$state.value, entity: value });
21
+ });
22
+ if (!entity) {
23
+ void this.fetch();
24
+ }
25
+ }
26
+ async fetch() {
27
+ const current = this.repository.get(this.table, this.id);
28
+ if (current) {
29
+ this.$state.next({ entity: current, status: "idle" });
30
+ return current;
31
+ }
32
+ this.$state.next({ ...this.$state.value, status: "fetching" });
33
+ try {
34
+ const value = await this.repository.fetch(this.table, this.id, this.fetcher);
35
+ this.$state.next({ entity: value, status: "idle" });
36
+ return value;
37
+ }
38
+ catch (error) {
39
+ if (error instanceof Error) {
40
+ this.$state.next({ entity: this.$state.value.entity, status: "error", error });
41
+ }
42
+ else {
43
+ this.$state.next({
44
+ entity: this.$state.value.entity,
45
+ status: "error",
46
+ error: new Error("RecordQuery fetch failed"),
47
+ });
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ dispose() {
53
+ this.subscription.unsubscribe();
54
+ }
55
+ }
@@ -0,0 +1,24 @@
1
+ import { BehaviorSubject, Subject } from "rxjs";
2
+ import type { EntityConfig, EntityDefinitions, EntityEvent, EntityIdTuple, RepositoryConfig } from "./types";
3
+ import { ListQuery, type ListQueryOptions } from "./list-query";
4
+ import { RecordQuery } from "./record-query";
5
+ export declare class Repository<Definitions extends EntityDefinitions, Config extends EntityConfig<Definitions> = EntityConfig<Definitions>> {
6
+ private stores;
7
+ private config;
8
+ constructor(config: RepositoryConfig<Definitions, Config>);
9
+ set<Table extends keyof Definitions>(table: Table, entity: Definitions[Table]): void;
10
+ del<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): void;
11
+ get<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): Definitions[Table] | null;
12
+ fetch<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>, fetcher: (id: EntityIdTuple<Definitions, Config, Table>) => Promise<Definitions[Table]>): Promise<Definitions[Table]>;
13
+ getObservable<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): BehaviorSubject<Definitions[Table] | null>;
14
+ getEvents<Table extends keyof Definitions>(table: Table): Subject<EntityEvent<Definitions[Table]>>;
15
+ recordQuery<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>, fetcher: (id: EntityIdTuple<Definitions, Config, Table>) => Promise<Definitions[Table]>): RecordQuery<Definitions, Config, Table>;
16
+ getCacheKey<Table extends keyof Definitions>(table: Table, id: EntityIdTuple<Definitions, Config, Table>): string;
17
+ getEntityKey<Table extends keyof Definitions>(table: Table, entity: Definitions[Table]): string;
18
+ listQuery<Table extends keyof Definitions>(table: Table, options: ListQueryOptions<Definitions[Table]>, fetcher: () => Promise<Definitions[Table][]>): ListQuery<Definitions, Config, Table>;
19
+ private getStore;
20
+ private getIdKey;
21
+ private getCacheKeyFromId;
22
+ private getCacheKeyFromEntity;
23
+ }
24
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ import { BehaviorSubject, Subject } from "rxjs";
2
+ import { ListQuery } from "./list-query";
3
+ import { RecordQuery } from "./record-query";
4
+ export class Repository {
5
+ stores = new Map();
6
+ config;
7
+ constructor(config) {
8
+ this.config = config;
9
+ }
10
+ set(table, entity) {
11
+ const store = this.getStore(table);
12
+ const cacheKey = this.getCacheKeyFromEntity(table, entity);
13
+ const existing = store.records.get(cacheKey);
14
+ store.records.set(cacheKey, entity);
15
+ if (existing) {
16
+ store.events$.next({
17
+ timestamp: new Date(),
18
+ type: "update",
19
+ old: existing,
20
+ new: entity,
21
+ });
22
+ }
23
+ else {
24
+ store.events$.next({
25
+ timestamp: new Date(),
26
+ type: "insert",
27
+ new: entity,
28
+ });
29
+ }
30
+ const subject = store.subjects.get(cacheKey);
31
+ if (subject) {
32
+ subject.next(entity);
33
+ }
34
+ }
35
+ del(table, id) {
36
+ const store = this.getStore(table);
37
+ const cacheKey = this.getCacheKeyFromId(table, id);
38
+ const existing = store.records.get(cacheKey);
39
+ store.records.delete(cacheKey);
40
+ if (existing) {
41
+ store.events$.next({
42
+ timestamp: new Date(),
43
+ type: "delete",
44
+ old: existing,
45
+ });
46
+ }
47
+ const subject = store.subjects.get(cacheKey);
48
+ if (subject) {
49
+ subject.next(null);
50
+ }
51
+ }
52
+ get(table, id) {
53
+ const store = this.getStore(table);
54
+ const cacheKey = this.getCacheKeyFromId(table, id);
55
+ const record = store.records.get(cacheKey);
56
+ return record ?? null;
57
+ }
58
+ async fetch(table, id, fetcher) {
59
+ const store = this.getStore(table);
60
+ const cacheKey = this.getCacheKeyFromId(table, id);
61
+ const record = store.records.get(cacheKey);
62
+ if (record) {
63
+ return record;
64
+ }
65
+ const inflight = store.inflight.get(cacheKey);
66
+ if (inflight) {
67
+ return inflight;
68
+ }
69
+ const request = (async () => {
70
+ const value = await fetcher(id);
71
+ this.set(table, value);
72
+ return value;
73
+ })();
74
+ store.inflight.set(cacheKey, request);
75
+ request.finally(() => {
76
+ store.inflight.delete(cacheKey);
77
+ });
78
+ return request;
79
+ }
80
+ getObservable(table, id) {
81
+ const store = this.getStore(table);
82
+ const cacheKey = this.getCacheKeyFromId(table, id);
83
+ const existing = store.subjects.get(cacheKey);
84
+ if (existing) {
85
+ return existing;
86
+ }
87
+ const record = store.records.get(cacheKey);
88
+ const subject = new BehaviorSubject(record ?? null);
89
+ store.subjects.set(cacheKey, subject);
90
+ return subject;
91
+ }
92
+ getEvents(table) {
93
+ const store = this.getStore(table);
94
+ return store.events$;
95
+ }
96
+ recordQuery(table, id, fetcher) {
97
+ return new RecordQuery(this, table, id, fetcher);
98
+ }
99
+ getCacheKey(table, id) {
100
+ return this.getCacheKeyFromId(table, id);
101
+ }
102
+ getEntityKey(table, entity) {
103
+ return this.getCacheKeyFromEntity(table, entity);
104
+ }
105
+ listQuery(table, options, fetcher) {
106
+ return new ListQuery(this, table, options, fetcher);
107
+ }
108
+ getStore(table) {
109
+ const existing = this.stores.get(table);
110
+ if (existing) {
111
+ return existing;
112
+ }
113
+ const store = {
114
+ records: new Map(),
115
+ subjects: new Map(),
116
+ inflight: new Map(),
117
+ events$: new Subject(),
118
+ };
119
+ this.stores.set(table, store);
120
+ return store;
121
+ }
122
+ getIdKey(table) {
123
+ return this.config.entities[table].id;
124
+ }
125
+ getCacheKeyFromId(table, id) {
126
+ const idKey = this.getIdKey(table);
127
+ const idValue = id[idKey];
128
+ if (idValue === undefined || idValue === null) {
129
+ throw new Error(`Missing identifier "${String(idKey)}" for ${String(table)}`);
130
+ }
131
+ return String(idValue);
132
+ }
133
+ getCacheKeyFromEntity(table, entity) {
134
+ const idKey = this.getIdKey(table);
135
+ const idValue = entity[idKey];
136
+ if (idValue === undefined || idValue === null) {
137
+ throw new Error(`Missing identifier "${String(idKey)}" for ${String(table)}`);
138
+ }
139
+ return String(idValue);
140
+ }
141
+ }
@@ -0,0 +1,34 @@
1
+ export type EntityDefinitions = Record<string, Record<string, unknown>>;
2
+ export type EntityConfig<Definitions extends EntityDefinitions> = {
3
+ [Table in keyof Definitions]: {
4
+ id: keyof Definitions[Table] & string;
5
+ };
6
+ };
7
+ export type RepositoryConfig<Definitions extends EntityDefinitions, Config extends EntityConfig<Definitions>> = {
8
+ entities: Config;
9
+ };
10
+ export type EntityIdTuple<Definitions extends EntityDefinitions, Config extends EntityConfig<Definitions>, Table extends keyof Definitions> = Pick<Definitions[Table], Config[Table]["id"]>;
11
+ export type EntityEvent<Entity> = {
12
+ timestamp: Date;
13
+ } & ({
14
+ type: "insert";
15
+ new: Entity;
16
+ } | {
17
+ type: "update";
18
+ old: Entity;
19
+ new: Entity;
20
+ } | {
21
+ type: "delete";
22
+ old: Entity;
23
+ });
24
+ export type RepositoryQuery<Entity> = {
25
+ entity: Entity | null;
26
+ } & ({
27
+ status: "fetching";
28
+ } | {
29
+ status: "idle";
30
+ } | {
31
+ status: "error";
32
+ error: Error;
33
+ });
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExE,MAAM,MAAM,YAAY,CAAC,WAAW,SAAS,iBAAiB,IAAI;KAC/D,KAAK,IAAI,MAAM,WAAW,GAAG;QAAE,EAAE,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,MAAM,CAAA;KAAE;CACxE,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAC1B,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,IACtC;IACF,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,CACvB,WAAW,SAAS,iBAAiB,EACrC,MAAM,SAAS,YAAY,CAAC,WAAW,CAAC,EACxC,KAAK,SAAS,MAAM,WAAW,IAC7B,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAElD,MAAM,MAAM,WAAW,CAAC,MAAM,IAAI;IAChC,SAAS,EAAE,IAAI,CAAC;CACjB,GAAG,CACA;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,MAAM,IAAI;IACpC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,GAAG,CACA;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,GACtB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CACpC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "entity-repository",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe entity caching and state management with RxJS and React",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "clean": "rm -rf dist",
20
+ "prepublishOnly": "npm run clean && npm run build"
21
+ },
22
+ "keywords": [
23
+ "entity",
24
+ "repository",
25
+ "cache",
26
+ "rxjs",
27
+ "react",
28
+ "state-management",
29
+ "typescript"
30
+ ],
31
+ "author": "Kurt Lee",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/breath103/entity-repository.git"
36
+ },
37
+ "homepage": "https://github.com/breath103/entity-repository#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/breath103/entity-repository/issues"
40
+ },
41
+ "dependencies": {
42
+ "rxjs": "^7.8.1"
43
+ },
44
+ "peerDependencies": {
45
+ "react": ">=18.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "react": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@types/react": "^19.2.8",
54
+ "typescript": "^5.9.3"
55
+ }
56
+ }