@prometheus-ags/prometheus-entity-management 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,119 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+
6
+ ---
7
+
8
+ ## [1.0.0] — 2026-04-04
9
+
10
+ Production-ready semantic version with CI, tests, documentation, and skills export verification.
11
+
12
+ ### Added
13
+
14
+ - Vitest smoke tests for `graph`, `engine`, and `RealtimeManager`.
15
+ - `pnpm run test`, `refresh:exports`, `verify:skills` scripts; `prepublishOnly` runs typecheck, build, test, and skills verification.
16
+ - `skills/_shared/references/library-exports.json` ledger (generated from `dist/index.mjs`) for agent skill ↔ runtime export alignment.
17
+ - GitHub Actions workflow: install, typecheck, build, test, verify:skills, typecheck for Vite and Next.js examples.
18
+ - Docs: `docs/tanstack-query-and-table.md`, `docs/advanced.md`, `RELEASING.md`; README documentation map and honest bundle-size guidance.
19
+ - Vite example route `/tanstack-bridge`: TanStack Query + sync into `upsertEntity`; example READMEs for Vite and Next.js.
20
+
21
+ ### Changed
22
+
23
+ - README comparison table: bundle size row points to measured guidance instead of a fixed “~15KB” claim.
24
+
25
+ ---
26
+
27
+ ## [0.1.0] — 2025-01-15
28
+
29
+ Initial release.
30
+
31
+ ### Added
32
+
33
+ **Core graph (`src/graph.ts`)**
34
+ - Zustand store with immer middleware for immutable entity mutations
35
+ - `entities[type][id]` — normalized entity storage
36
+ - `patches[type][id]` — local UI augmentation layer, merged at read time
37
+ - `lists[queryKey]` — list state holding ordered ID arrays (refs, not copies)
38
+ - `invalidateEntity`, `invalidateLists`, `invalidateType` — stale marking
39
+ - `removeIdFromAllLists` — surgical list cleanup on entity delete
40
+
41
+ **Engine (`src/engine.ts`)**
42
+ - In-flight deduplication via process-global `Map<key, Promise>`
43
+ - Subscriber ref-counting via `Symbol` tokens
44
+ - `fetchEntity` and `fetchList` with exponential backoff retry
45
+ - `attachGlobalListeners` for focus/reconnect revalidation
46
+ - `configureEngine` for global defaults
47
+
48
+ **Hooks (`src/hooks.ts`)**
49
+ - `useEntity<TRaw, TEntity>` — single entity fetch and subscription
50
+ - `useEntityList<TRaw, TEntity>` — list fetch with mode=replace|append
51
+ - `useEntityMutation<TInput, TRaw, TEntity>` — optimistic mutations with rollback
52
+ - `useEntityAugment<TEntity>` — local UI patches visible to all entity subscribers
53
+
54
+ **View layer (`src/view/`)**
55
+ - `FilterSpec` — transport-agnostic filter description (and/or, 16 operators)
56
+ - `SortSpec` — multi-field sort with null handling and custom comparators
57
+ - `toRestParams` — FilterSpec → REST query string params
58
+ - `toGraphQLVariables` — FilterSpec → Hasura/Postgraphile-style GQL variables
59
+ - `toSQLClauses` — FilterSpec → parameterized SQL WHERE + ORDER BY
60
+ - `matchesFilter`, `matchesSearch`, `compareEntities` — local JS evaluation
61
+ - `findInsertionIndex` — O(log n) binary search for realtime sorted insertion
62
+ - `useEntityView` — local/remote/hybrid completeness mode, debounced remote fetch,
63
+ realtime filter evaluation and sorted insertion, `setFilter/setSort/setSearch`
64
+
65
+ **CRUD lifecycle (`src/crud/`)**
66
+ - `registerSchema` / `getSchema` — relation schema registry
67
+ - `cascadeInvalidation` — automatic stale marking on mutation, follows FK changes
68
+ - `readRelations` — resolve related entities from graph using schema
69
+ - `useEntityCRUD` — unified list+detail+edit+create+delete hook
70
+ - Isolated edit buffer (never bleeds to other views until `save()`)
71
+ - Field-level dirty tracking (`dirty.changed: Set<keyof T>`)
72
+ - Optimistic create with temp ID insertion
73
+ - Optimistic delete with rollback on failure
74
+ - `applyOptimistic()` for instant cross-view feedback on toggle/slider fields
75
+ - `cascadeInvalidation` fires automatically after every mutation
76
+
77
+ **Realtime adapters (`src/adapters/`)**
78
+ - `RealtimeAdapter` / `SyncAdapter` interfaces
79
+ - `RealtimeManager` — change coalescing (16ms flush window), adapter registry
80
+ - `createWebSocketAdapter` — reconnect with exponential backoff, ping/keepalive
81
+ - `createSupabaseRealtimeAdapter` — Postgres Changes via logical replication
82
+ - `createConvexAdapter` — snapshot diffing for reactive query results
83
+ - `createGraphQLSubscriptionAdapter` — graphql-ws protocol
84
+ - `createElectricAdapter` — PGlite + ElectricSQL shape sync + NOTIFY listener
85
+ - `useLocalFirst` — isSynced state, local query/execute surface
86
+ - `usePGliteQuery` — raw SQL → entity graph population
87
+
88
+ **GraphQL (`src/graphql/`)**
89
+ - `GQLClient` — query/mutate/subscribe with `EntityDescriptor` normalization
90
+ - `normalizeGQLResponse` — recursive response walker, writes to entity graph
91
+ - `createGQLClient` factory
92
+ - `useGQLEntity` — mirrors `useEntity` over GraphQL
93
+ - `useGQLList` — mirrors `useEntityList` over GraphQL with cursor pagination
94
+ - `useGQLMutation` — mirrors `useEntityMutation` over GraphQL
95
+ - `useGQLSubscription` — graphql-ws subscription → entity graph updates
96
+
97
+ **UI layer (`src/ui/`)**
98
+ - `selectionColumn`, `textColumn`, `numberColumn`, `dateColumn`, `enumColumn`,
99
+ `booleanColumn`, `actionsColumn` — typed TanStack Table column builders
100
+ - `SortHeader` — sort indicator button wired to column sort state
101
+ - `EntityTable` — full table component with TanStack Table, inline editing,
102
+ load-more / page pagination, skeleton loading, empty state, toolbar
103
+ - `InlineCellEditor` — double-click cell editing with Enter/Escape handling
104
+ - `Sheet` — side drawer with backdrop, keyboard dismiss, footer slot
105
+ - `EntityDetailSheet` — detail view with edit/delete actions, field rendering,
106
+ delete confirmation dialog
107
+ - `EntityFormSheet` — create/edit form with field dirty indicators, error display
108
+
109
+ **Examples**
110
+ - `examples/vite-app` — React 19 + Vite 6, TanStack Router, full CRUD demo
111
+ (Dashboard, Projects, Tasks, Team) with Prometheus AGS mock data
112
+ - `examples/nextjs-app` — Next.js 15, Server Component SSR hydration,
113
+ `GraphHydrationProvider`, product catalog with REST API routes
114
+
115
+ ---
116
+
117
+ ## Roadmap
118
+
119
+ See README.md § Roadmap.
package/README.md ADDED
@@ -0,0 +1,525 @@
1
+ # @prometheus-ags/prometheus-entity-management
2
+
3
+ **Normalized, globally-reactive entity graph store for React**
4
+
5
+ Update a post in one screen and every list row, detail panel, and badge that reads that entity updates automatically—without hand-maintained query keys. Normalization is built around your `type` + `id` + `normalize` function, not a separate cache product. The same graph holds data from **REST**, **GraphQL**, **WebSocket / Supabase / Convex**, **Prisma-shaped APIs**, and **ElectricSQL + PGlite** local-first sync.
6
+
7
+ ### Documentation map
8
+
9
+ | Doc | Purpose |
10
+ |-----|---------|
11
+ | [docs/tanstack-query-and-table.md](docs/tanstack-query-and-table.md) | How this library fits with TanStack Query and TanStack Table |
12
+ | [docs/advanced.md](docs/advanced.md) | Engine, GC, Suspense, DevTools, SSR, testing |
13
+ | [RELEASING.md](RELEASING.md) | Versioning, `prepublishOnly`, npm publish |
14
+ | [CHANGELOG.md](CHANGELOG.md) | Release history |
15
+
16
+ ---
17
+
18
+ ## Quick start
19
+
20
+ ### 1. Install
21
+
22
+ ```bash
23
+ npm install @prometheus-ags/prometheus-entity-management zustand immer
24
+ ```
25
+
26
+ (`pnpm` and `yarn` work the same way for app dependencies.)
27
+
28
+ ### 2. Define an entity type
29
+
30
+ ```ts
31
+ type Post = { id: string; title: string; status: string };
32
+ ```
33
+
34
+ ### 3. Fetch and render
35
+
36
+ ```tsx
37
+ import { useEntity } from "@prometheus-ags/prometheus-entity-management";
38
+
39
+ export function PostCard({ postId }: { postId: string }) {
40
+ const { data, isLoading, error } = useEntity<Post, Post>({
41
+ type: "Post",
42
+ id: postId,
43
+ fetch: async (id) => {
44
+ const res = await fetch(`/api/posts/${id}`);
45
+ if (!res.ok) throw new Error(String(res.status));
46
+ return res.json() as Post;
47
+ },
48
+ normalize: (raw) => raw,
49
+ });
50
+
51
+ if (isLoading) return <p>Loading…</p>;
52
+ if (error) return <p>{error}</p>;
53
+ if (!data) return null;
54
+ return <article>{data.title}</article>;
55
+ }
56
+ ```
57
+
58
+ Any other component that calls `useEntity` with the same `type` and `id` reads the **same** normalized record from the graph.
59
+
60
+ ---
61
+
62
+ ## Core concepts
63
+
64
+ ### Entities live exactly once
65
+
66
+ Each `(entityType, id)` maps to a single canonical object in the Zustand graph (`entities[type][id]`). Lists and detail views never keep their own full copies; they resolve through that node.
67
+
68
+ ### Queries are instructions, not containers
69
+
70
+ `useEntity`, `useEntityList`, and GraphQL hooks describe **how** to load data and **how** to normalize it into the graph. They do not own isolated cache entries the way query-key–scoped caches do.
71
+
72
+ ### Lists store IDs, not data
73
+
74
+ List state keeps ordered **IDs** plus pagination metadata. Rows are `items` joined from `entities` at render time, so when `Post:123` changes, every list that includes that ID re-renders consistently.
75
+
76
+ ### Three-layer model
77
+
78
+ Data flows **up** into the graph; UI reads **down** through hooks (see [Architecture](#architecture)).
79
+
80
+ ```
81
+ ┌─────────────────────────────────────────────────────────────┐
82
+ │ Layer 3: UI Components (optional, users can build their own)│
83
+ │ src/ui/ │
84
+ │ EntityTable · EntityDetailSheet · EntityFormSheet · columns │
85
+ ├─────────────────────────────────────────────────────────────┤
86
+ │ Layer 2: Access Patterns (hooks - how components read data) │
87
+ │ src/hooks.ts, src/graphql/hooks.ts, src/crud/ │
88
+ │ useEntity · useEntityList · useEntityView · useEntityCRUD │
89
+ │ useGQLEntity · useEntityMutation · useEntityAugment │
90
+ ├─────────────────────────────────────────────────────────────┤
91
+ │ Layer 1: Entity Graph (Zustand store - canonical data) │
92
+ │ src/graph.ts │
93
+ │ entities[type][id] · patches[type][id] · lists[queryKey] │
94
+ └─────────────────────────────────────────────────────────────┘
95
+ ▲ ▲ ▲ ▲
96
+ REST fetch GraphQL WebSocket ElectricSQL
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Feature comparison
102
+
103
+ | Feature | `@prometheus-ags/prometheus-entity-management` | TanStack Query | Apollo Client | SWR |
104
+ |--------|--------------------------------------------------|----------------|---------------|-----|
105
+ | Normalized cache | Yes (automatic) | No (manual) | Yes (manual config) | No |
106
+ | Cross-view reactivity | Yes | No | Partial | No |
107
+ | REST support | Yes | Yes | No | Yes |
108
+ | GraphQL support | Yes | No (separate client) | Yes | No |
109
+ | Realtime / WebSocket | Yes (built-in adapters) | No (manual) | Yes (subscriptions) | No |
110
+ | Local-first (ElectricSQL) | Yes | No | No | No |
111
+ | Prisma integration | Yes | No | No | No |
112
+ | CRUD lifecycle | Yes (`useEntityCRUD`) | No | No | No |
113
+ | Relation schemas | Yes (cascade invalidation) | No | Yes (type policies) | No |
114
+ | Suspense hooks | Yes | Yes | Yes | Yes |
115
+ | SSR hydration | Yes | Yes | Yes | Yes |
116
+ | Garbage collection | Yes (automatic, configurable) | Yes | Yes | No |
117
+ | Bundle size | See [Bundle size](#bundle-size) | ~39KB | ~130KB | ~4KB |
118
+
119
+ Peer dependencies (`react`, `react-dom`, optional `@tanstack/react-table`) are **not** included in any column. Published `dist` sizes change with each release—measure before quoting numbers in docs or talks.
120
+
121
+ ### Bundle size
122
+
123
+ The npm package ships a **single large entry** (`dist/index.mjs`) that re-exports the full surface (hooks, GraphQL, CRUD, view, UI, adapters). **Your** app’s gzipped cost depends on **tree-shaking**, **minification**, and **which imports you use**.
124
+
125
+ **Maintainers:** after `pnpm run build`, a rough gzip size of the ESM bundle is:
126
+
127
+ ```bash
128
+ gzip -c dist/index.mjs | wc -c
129
+ ```
130
+
131
+ Compare against peers only when measurement methodology matches (minified vs unminified, gzip vs brotli, ESM vs CJS).
132
+
133
+ ---
134
+
135
+ ## API reference (brief)
136
+
137
+ ### Core
138
+
139
+ | Export | Description |
140
+ |--------|-------------|
141
+ | `useGraphStore` | Zustand store: `entities`, `patches`, `lists`, and graph mutations. Prefer hooks in UI; `getState()` is for effects/adapters. |
142
+ | `configureEngine` | App-wide defaults: stale time, retries, GC interval, GC time, etc. |
143
+ | `getEngineOptions` | Read merged engine options. |
144
+ | `serializeKey` | Stable string key for list `queryKey` serialization. |
145
+ | `fetchEntity` | Imperative single-entity fetch with dedupe and graph write (for custom hooks/adapters). |
146
+ | `fetchList` | Imperative list fetch with dedupe and graph write. |
147
+ | `dedupe` | Process-global in-flight promise deduplication helper. |
148
+ | `startGarbageCollector` / `stopGarbageCollector` | Periodic eviction of unsubscribed, stale entities (also started via `configureEngine`). |
149
+
150
+ ### Hooks (REST-oriented)
151
+
152
+ | Export | Description |
153
+ |--------|-------------|
154
+ | `useEntity` | Subscribe to one entity; fetch/normalize into graph; SWR + subscriber-aware refetch. |
155
+ | `useEntityList` | Subscribe to a list query key; stores IDs; merges row data from graph. |
156
+ | `useEntityMutation` | Mutate with optional optimistic updates and list invalidation hooks. |
157
+ | `useEntityAugment` | Patch UI-only fields merged at read time across all subscribers. |
158
+ | `useSuspenseEntity` | Suspense variant of `useEntity` (non-null `id` required). |
159
+ | `useSuspenseEntityList` | Suspense variant of `useEntityList`. |
160
+
161
+ ### View
162
+
163
+ | Export | Description |
164
+ |--------|-------------|
165
+ | `useEntityView` | Filter/sort/search with `local` / `remote` / `hybrid` completeness modes. |
166
+ | `FilterSpec`, `SortSpec` | Transport-agnostic filter and sort AST types. |
167
+ | `toRestParams` | Compile view → REST query params. |
168
+ | `toSQLClauses` | Compile view → SQL-style WHERE / ORDER BY fragments. |
169
+ | `toGraphQLVariables` | Compile view → common GraphQL variable shapes. |
170
+ | `toPrismaWhere` / `toPrismaOrderBy` | Compile view → Prisma-style `where` / `orderBy` objects. |
171
+ | `applyView`, `compareEntities`, `matchesFilter`, `matchesSearch`, `checkCompleteness` | Local evaluation and completeness helpers. |
172
+ | `flattenClauses`, `hasCustomPredicates` | Filter introspection utilities. |
173
+
174
+ ### CRUD
175
+
176
+ | Export | Description |
177
+ |--------|-------------|
178
+ | `useEntityCRUD` | Unified list + detail + edit + create flow, edit buffer, dirty tracking, optimistic helpers. |
179
+ | `registerSchema` | Register entity relations for cascade invalidation after mutations. |
180
+ | `getSchema`, `readRelations`, `cascadeInvalidation` | Introspection and imperative cascade invalidation. |
181
+
182
+ ### Realtime
183
+
184
+ | Export | Description |
185
+ |--------|-------------|
186
+ | `RealtimeManager` | Registers adapters, coalesces changes (16 ms window), writes to graph. |
187
+ | `getRealtimeManager`, `resetRealtimeManager` | Singleton access and test resets. |
188
+ | `createWebSocketAdapter` | Generic WebSocket → graph changes. |
189
+ | `createSupabaseRealtimeAdapter` | Supabase Realtime payloads → graph. |
190
+ | `createConvexAdapter` | Convex-shaped streams → graph. |
191
+ | `createGraphQLSubscriptionAdapter` | GraphQL over WebSocket subscriptions → graph. |
192
+
193
+ ### GraphQL
194
+
195
+ | Export | Description |
196
+ |--------|-------------|
197
+ | `createGQLClient` | Configure endpoint, fetcher, and entity descriptors for normalization. |
198
+ | `GQLClient` | Client class instance type. |
199
+ | `normalizeGQLResponse` / `executeGQL` | Normalize and execute with the same descriptor model. |
200
+ | `useGQLEntity`, `useGQLList` | Graph-backed entity and list hooks. |
201
+ | `useGQLMutation`, `useGQLSubscription` | GraphQL mutation and subscription hooks tied to the graph. |
202
+
203
+ ### Prisma
204
+
205
+ | Export | Description |
206
+ |--------|-------------|
207
+ | `createPrismaEntityConfig` | Factory for REST endpoints that speak Prisma-style `where` / `orderBy` query params. |
208
+ | `prismaRelationsToSchema` | Convert Prisma-style relation map → `EntitySchema` for `registerSchema`. |
209
+ | `toPrismaInclude` | Build an `include` map from relation descriptors. |
210
+
211
+ ### Local-first
212
+
213
+ | Export | Description |
214
+ |--------|-------------|
215
+ | `createElectricAdapter` | ElectricSQL / PGlite changes → graph. |
216
+ | `useLocalFirst` | Hook for local-first workflows with the adapter. |
217
+ | `usePGliteQuery` | Run queries against PGlite in sync with the graph story. |
218
+
219
+ ### DevTools
220
+
221
+ | Export | Description |
222
+ |--------|-------------|
223
+ | `useGraphDevTools` | Hook for debugging graph shape and activity in development. |
224
+
225
+ ### UI (optional)
226
+
227
+ | Export | Description |
228
+ |--------|-------------|
229
+ | `EntityTable`, `InlineCellEditor` | Table + inline cell editing wired to the graph / view layer. |
230
+ | `EntityDetailSheet`, `EntityFormSheet`, `Sheet` | CRUD-oriented sheet primitives. |
231
+ | `selectionColumn`, `textColumn`, `numberColumn`, `dateColumn`, `enumColumn`, `booleanColumn`, `actionsColumn`, `SortHeader` | Column helpers with filter metadata for tooling. |
232
+
233
+ ### Types (high level)
234
+
235
+ `GraphState`, `EntityState`, `ListState`, `EntityType`, `EntityId`, `EngineOptions`, `EntityQueryOptions`, `ListQueryOptions`, `ViewDescriptor`, `EntitySchema`, `RelationDescriptor`, realtime adapter types, GraphQL types, CRUD types, column meta types — all exported from the package entry.
236
+
237
+ ---
238
+
239
+ ## Migration from TanStack Query
240
+
241
+ ### Single record: `useQuery` → `useEntity`
242
+
243
+ **Before (TanStack Query)**
244
+
245
+ ```tsx
246
+ const { data, isLoading } = useQuery({
247
+ queryKey: ["post", id],
248
+ queryFn: () => fetch(`/api/posts/${id}`).then((r) => r.json()),
249
+ });
250
+ ```
251
+
252
+ **After (entity graph)**
253
+
254
+ ```tsx
255
+ const { data, isLoading } = useEntity<Post, Post>({
256
+ type: "Post",
257
+ id,
258
+ fetch: (postId) => fetch(`/api/posts/${postId}`).then((r) => r.json()),
259
+ normalize: (raw) => raw,
260
+ });
261
+ ```
262
+
263
+ **Difference:** the graph key is `(type, id)`, not an opaque query key. Anything else that uses the same `type`/`id` shares that record—no `setQueryData` across keys.
264
+
265
+ ### Lists: `useQuery` + key → `useEntityList`
266
+
267
+ **Before**
268
+
269
+ ```tsx
270
+ const { data } = useQuery({
271
+ queryKey: ["posts", { status }],
272
+ queryFn: () => api.posts.list({ status }),
273
+ });
274
+ ```
275
+
276
+ **After**
277
+
278
+ ```tsx
279
+ const { items, isLoading } = useEntityList<Post, Post>({
280
+ type: "Post",
281
+ queryKey: ["posts", { status }],
282
+ fetch: (p) => api.posts.list({ status, page: p.page, pageSize: p.pageSize, cursor: p.cursor }),
283
+ normalize: (row) => ({ id: row.id, data: row }),
284
+ });
285
+ ```
286
+
287
+ **Difference:** the list stores **IDs**; row objects are always read through the normalized `Post` map, so updates propagate everywhere.
288
+
289
+ ### Mutations: `useMutation` → `useEntityMutation`
290
+
291
+ **Before**
292
+
293
+ ```tsx
294
+ const qc = useQueryClient();
295
+ const mutation = useMutation({
296
+ mutationFn: (id: string) => api.posts.archive(id),
297
+ onSuccess: () => {
298
+ qc.invalidateQueries({ queryKey: ["posts"] });
299
+ qc.invalidateQueries({ queryKey: ["post"] });
300
+ },
301
+ });
302
+ ```
303
+
304
+ **After**
305
+
306
+ ```tsx
307
+ import { serializeKey, useEntityMutation } from "@prometheus-ags/prometheus-entity-management";
308
+
309
+ const { mutate } = useEntityMutation<string, Post, Post>({
310
+ type: "Post",
311
+ mutate: (id) => api.posts.archive(id),
312
+ normalize: (raw) => ({ id: raw.id, data: raw }),
313
+ optimistic: (id) => ({ id, patch: { status: "archived" } }),
314
+ invalidateLists: [serializeKey(["posts"])],
315
+ });
316
+ ```
317
+
318
+ **Difference:** optimistic updates target the **entity**; optional list invalidation is declarative. Cross-view consistency comes from normalization, not from remembering every query key.
319
+
320
+ ---
321
+
322
+ ## Migration from Apollo Client
323
+
324
+ ### Query: `useQuery` → `useGQLEntity` / `useGQLList`
325
+
326
+ **Before (Apollo)**
327
+
328
+ ```tsx
329
+ const { data } = useQuery(GET_POST, { variables: { id } });
330
+ ```
331
+
332
+ **After**
333
+
334
+ ```tsx
335
+ const { data } = useGQLEntity({
336
+ client: gqlClient,
337
+ document: GET_POST,
338
+ variables: {},
339
+ type: "Post",
340
+ id,
341
+ descriptor: postDescriptor,
342
+ });
343
+ ```
344
+
345
+ Use `useGQLList` with `document`, `queryKey`, `descriptor`, and `getItems` to map the response into rows.
346
+
347
+ **Difference:** you describe entities with **descriptors** (how to normalize IDs and nested types) once; you do not maintain a parallel universe of type policies and merge functions for every edge case.
348
+
349
+ ### Mutation: `useMutation` → `useGQLMutation`
350
+
351
+ **Before**
352
+
353
+ ```tsx
354
+ const [mutate] = useMutation(UPDATE_POST);
355
+ ```
356
+
357
+ **After**
358
+
359
+ ```tsx
360
+ const { mutate } = useGQLMutation({
361
+ client: gqlClient,
362
+ document: UPDATE_POST,
363
+ type: "Post",
364
+ descriptors: [postDescriptor],
365
+ });
366
+ ```
367
+
368
+ Descriptors tell the client how to write normalized entities from the mutation payload—no Apollo-style type policies.
369
+
370
+ ### Subscriptions: `useSubscription` → `useGQLSubscription`
371
+
372
+ **Before**
373
+
374
+ ```tsx
375
+ useSubscription(POST_UPDATED, { variables: { id } });
376
+ ```
377
+
378
+ **After**
379
+
380
+ ```tsx
381
+ useGQLSubscription({
382
+ client: gqlClient,
383
+ wsClient: gqlWsClient,
384
+ document: POST_UPDATED_SUB,
385
+ variables: { id },
386
+ descriptors: [postDescriptor],
387
+ });
388
+ ```
389
+
390
+ **Difference:** GraphQL, REST, and realtime adapters can all write the **same** entity graph, so mixed stacks do not need two caches.
391
+
392
+ ---
393
+
394
+ ## Prisma integration
395
+
396
+ `createPrismaEntityConfig` targets REST APIs that accept Prisma-style `where` and `orderBy` as JSON query parameters (typical for Prisma-backed route handlers).
397
+
398
+ ```typescript
399
+ import {
400
+ createPrismaEntityConfig,
401
+ registerSchema,
402
+ useEntity,
403
+ useEntityList,
404
+ } from "@prometheus-ags/prometheus-entity-management";
405
+
406
+ type Post = { id: string; title: string; authorId: string };
407
+
408
+ const Posts = createPrismaEntityConfig<Post>({
409
+ type: "Post",
410
+ endpoint: "/api/posts",
411
+ relations: {
412
+ author: { type: "User", foreignKey: "authorId", relation: "belongsTo" },
413
+ comments: { type: "Comment", foreignKey: "postId", relation: "hasMany" },
414
+ },
415
+ });
416
+
417
+ // Register cascade rules once (e.g. app init)
418
+ Posts.schemas().forEach(registerSchema);
419
+ ```
420
+
421
+ ```tsx
422
+ function PostDetail({ postId }: { postId: string }) {
423
+ const { data } = useEntity(Posts.entity(postId));
424
+ return data ? <h1>{data.title}</h1> : null;
425
+ }
426
+
427
+ function PostList() {
428
+ const { items } = useEntityList(
429
+ Posts.list({
430
+ filter: [{ field: "status", op: "eq", value: "published" }],
431
+ sort: [{ field: "createdAt", direction: "desc" }],
432
+ })
433
+ );
434
+ return (
435
+ <ul>
436
+ {items.map((p) => (
437
+ <li key={p.id}>{p.title}</li>
438
+ ))}
439
+ </ul>
440
+ );
441
+ }
442
+ ```
443
+
444
+ Use `Posts.crud()` with `useEntityCRUD` when you want the full list + detail + forms pipeline against the same endpoints.
445
+
446
+ ---
447
+
448
+ ## Examples
449
+
450
+ | Example | Path | What it demonstrates |
451
+ |--------|------|---------------------|
452
+ | **Vite app** | [`examples/vite-app/`](examples/vite-app/) | Full CRUD, realtime adapters, **TanStack Query → graph bridge** (`/tanstack-bridge`), `EntityTable` / sheets, mock API with latency |
453
+ | **Next.js app** | [`examples/nextjs-app/`](examples/nextjs-app/) | Same feature set as the Vite example (Project/Task/User CRUD, realtime, engine settings, pure list view, TanStack Query → graph bridge). **SSR:** `GraphHydrationProvider` seeds the client graph from the shared demo data on first load |
454
+
455
+ From the repo root (this monorepo uses **pnpm**):
456
+
457
+ ```bash
458
+ pnpm install
459
+ pnpm run dev:vite # http://localhost:5173
460
+ pnpm run dev:next # http://localhost:3000
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Architecture
466
+
467
+ ### Data flow rules
468
+
469
+ - **Components → Hooks → Stores → APIs / realtime** — UI uses hooks only; hooks orchestrate; network and adapters update the graph.
470
+ - **Up into the graph:** fetches, mutations, and realtime events call into the Zustand store.
471
+ - **Down from hooks:** `useEntity`, `useEntityList`, `useEntityView`, GraphQL hooks, and CRUD read merged `entities` + `patches`.
472
+
473
+ ### Graph structures (`src/graph.ts`)
474
+
475
+ 1. **`entities`** — Canonical server-shaped records per `(type, id)`.
476
+ 2. **`patches`** — Local-only overlays (`_selected`, `_loading`, …) merged at read time.
477
+ 3. **`lists`** — Ordered `ids[]`, pagination, and fetch flags — **not** duplicated row payloads.
478
+
479
+ ### Engine (`src/engine.ts`)
480
+
481
+ In-flight deduplication, retries, subscriber ref-counting, stale-while-revalidate, optional periodic garbage collection for entities without subscribers.
482
+
483
+ ### Realtime (`src/adapters/realtime-manager.ts`)
484
+
485
+ Adapters emit a shared change shape; the manager batches updates per animation frame to avoid UI thrash.
486
+
487
+ ### View layer (`src/view/`)
488
+
489
+ One `FilterSpec` / `SortSpec` can compile to REST, SQL, GraphQL variables, or Prisma shapes, and can run locally when the graph already holds enough data.
490
+
491
+ ### CRUD (`src/crud/`)
492
+
493
+ `useEntityCRUD` keeps the edit buffer in React state so other views stay on committed data until save; `registerSchema` drives relation-aware cascade invalidation.
494
+
495
+ ---
496
+
497
+ ## Development (this repository)
498
+
499
+ ```bash
500
+ pnpm install
501
+
502
+ # Examples
503
+ pnpm run dev:vite
504
+ pnpm run dev:next
505
+
506
+ # Typecheck
507
+ pnpm run typecheck
508
+ pnpm run typecheck:vite
509
+ pnpm run typecheck:next
510
+
511
+ # Production builds of examples
512
+ pnpm run build:vite
513
+ pnpm run build:next
514
+
515
+ # Clean artifacts
516
+ pnpm run clean
517
+ ```
518
+
519
+ The library is consumed from source via path aliases in examples during development (no separate build step required for local hacking).
520
+
521
+ ---
522
+
523
+ ## License
524
+
525
+ MIT © Prometheus AGS / KnowMe LLC