@solidev/data 1.0.0-beta.1 → 1.0.0-beta.2

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/AGENTS.md ADDED
@@ -0,0 +1,475 @@
1
+ # Using `@solidev/data` — rules for AI agents
2
+
3
+ This file tells an AI coding agent how to use `@solidev/data` **correctly**. It is
4
+ shipped inside the published package, so it is available at
5
+ `node_modules/@solidev/data/AGENTS.md` in any consuming project.
6
+
7
+ If you are an agent working in a project that depends on `@solidev/data`, read this
8
+ before writing models, lists, or forms. The rules below are not style preferences —
9
+ most of them are runtime contracts that fail **silently** when broken.
10
+
11
+ > To adopt these rules in your project, see [Wiring this into your project](#wiring-this-into-your-project) at the end.
12
+
13
+ ---
14
+
15
+ ## 1. The mental model
16
+
17
+ `@solidev/data` is a **Django-REST-Framework-style client for Angular**. The chain is:
18
+
19
+ ```
20
+ DataModel → Collection<T> → Queryset<T> → ModelList<T> → components
21
+ (fields) (an endpoint) (filter/sort) (list state) (dispedit, filters…)
22
+ ```
23
+
24
+ The central idea: **you declare fields on a model, and the UI configures itself from
25
+ that metadata.** A `foreignKeyField` renders as an FK dropdown, a `charField` with
26
+ `choices` renders as a `<select>`, a `decimalField` renders as money — because the
27
+ field manager sets its own `editorType`/`displayType`. You almost never choose a
28
+ widget by hand.
29
+
30
+ So: **put the effort into the model declaration.** Everything downstream follows.
31
+
32
+ ---
33
+
34
+ ## 2. Declaring a model
35
+
36
+ ### 2.1 The four-file split
37
+
38
+ One directory per model, four files. This is the convention across every mature
39
+ consumer of this library:
40
+
41
+ ```
42
+ thing/
43
+ thing.base.ts # ThingBase extends DataModel — server-side fields only
44
+ thing.ts # Thing extends ThingBase — the *_details half of relations
45
+ thing.service.ts # ThingService extends Collection<Thing>
46
+ thing.resolver.ts # optional route resolver
47
+ ```
48
+
49
+ The split is **not cosmetic — it is the circular-import firewall.** `*.base.ts`
50
+ imports only `@solidev/data` plus local constants; it never imports a sibling model.
51
+ `*.ts` is the only file allowed to import other model classes. Two models can
52
+ reference each other because the id half of a relation only needs a *string*, never
53
+ the target class.
54
+
55
+ ### 2.2 `__name` is mandatory and must be unique
56
+
57
+ ```ts
58
+ export class ThingBase extends DataModel {
59
+ static override readonly __name: string = 'ThingBase';
60
+ }
61
+ export class Thing extends ThingBase {
62
+ static override readonly __name: string = 'Thing'; // redeclared, always
63
+ }
64
+ ```
65
+
66
+ Rules:
67
+
68
+ - Declare `__name` on **every** class in the chain, base and concrete.
69
+ - It must be **globally unique**.
70
+ - Keep the explicit `: string` annotation — it defeats literal-type narrowing so
71
+ `override` typechecks.
72
+
73
+ **Why it matters:** `DataModel`'s constructor walks the prototype chain and collects
74
+ field metadata by matching keys against `` `${parent.constructor.__name}__` ``. A
75
+ missing or duplicated `__name` **silently drops that class's fields**. There is no
76
+ error — you just get a model with missing fields.
77
+
78
+ ### 2.3 Never declare `id`
79
+
80
+ `id` is declared once on `DataModel` with `@primaryField()`. Every model inherits it.
81
+
82
+ ```ts
83
+ // ✅ correct — say nothing, you already have `id: number`
84
+ export class ThingBase extends DataModel { /* … */ }
85
+
86
+ // ❌ WRONG — shadows the base field with `undefined`
87
+ @primaryField() public override id!: number;
88
+ ```
89
+
90
+ Under `useDefineForClassFields: true` (which this library and Angular ≥ 20 use),
91
+ re-declaring `override id` emits `id = undefined` and destroys the inherited field.
92
+
93
+ ### 2.4 Field ordering and `priority`
94
+
95
+ Source order is for humans. **Display order comes from `priority` — higher shows
96
+ first.** `id` is `1000`.
97
+
98
+ `priority: -1` is the established idiom for **"load this field, never offer it in the
99
+ UI"**. Use it on the id half of every relation. A field with no `priority` sorts
100
+ unpredictably — always set one.
101
+
102
+ ---
103
+
104
+ ## 3. Relations: the FK ⇄ details pair
105
+
106
+ This is the single most important pattern in the library, and the easiest to get
107
+ wrong. **Every relation is two fields split across two files.**
108
+
109
+ ```ts
110
+ // thing.base.ts — the id half. Does NOT import Category.
111
+ @foreignKeyField({
112
+ name: 'category',
113
+ description: 'Category',
114
+ related: 'Category', // see the warning below
115
+ priority: -1, // the raw id is plumbing, not UI
116
+ })
117
+ public category!: number;
118
+ ```
119
+
120
+ ```ts
121
+ // thing.ts — the details half. Imports Category for real.
122
+ import { Category } from '../category/category';
123
+
124
+ @detailsField({
125
+ description: 'Category',
126
+ model: Category, // ← the REAL link
127
+ readonly: true,
128
+ priority: 750, // the displayed half of the pair
129
+ })
130
+ public category_details?: Category;
131
+ ```
132
+
133
+ ### 3.1 The `_details` name is a runtime contract
134
+
135
+ `dispedit` and `m2mselect` find the sibling by **string concatenation** —
136
+ `` (model as any)[`${field}_details`] ``. So the property must be named **exactly**
137
+ `<fkFieldName>_details`. Nothing type-checks this. Rename the FK without renaming
138
+ its details twin and both the display and the write-back break **silently**. There is
139
+ no alias mechanism.
140
+
141
+ ### 3.2 `related:` is inert — do not rely on it
142
+
143
+ **`related` is never read by the library.** It is unresolved, unvalidated, purely
144
+ documentary metadata. In a 150-model production codebase, roughly 20% of `related`
145
+ values point at classes that do not exist — misspellings and stale renames — and it
146
+ has never caused a bug, because nothing resolves it.
147
+
148
+ - Set it to the **concrete** class name (`'Category'`, never `'CategoryBase'`) as an
149
+ annotation for humans and tooling.
150
+ - **Never assume it links anything.** `model:` on the details half does the work.
151
+
152
+ ### 3.3 Many-to-many is the same shape
153
+
154
+ ```ts
155
+ // base — note defaultValue: []
156
+ @manyToManyField({ name: 'tags', description: 'Tags', related: 'Tag',
157
+ defaultValue: [], priority: -1 })
158
+ public tags: number[] = [];
159
+
160
+ // concrete — note many: true
161
+ @detailsField({ description: 'Tags', model: Tag, many: true,
162
+ readonly: true, priority: 650 })
163
+ public tags_details?: Tag[];
164
+ ```
165
+
166
+ **`defaultValue: []` is not optional in practice.** Omit it and the field
167
+ deserialises to `undefined`, which breaks the `m2mselect` editor.
168
+
169
+ ### 3.4 Self-reference needs nothing special
170
+
171
+ A tree model points at itself with `related: 'Category'` in `category.base.ts` — just
172
+ a string, so no cycle. The details half in `category.ts` uses `model: Category`, which
173
+ is fully defined by the time the decorator runs. **No `forwardRef`, no lambda.**
174
+ `model:` is always a bare class identifier.
175
+
176
+ ### 3.5 `detailsField` without `model:`
177
+
178
+ A bare `@detailsField({...})` with no `model:` **passes the raw JSON through
179
+ untouched**. That is the right tool for server-computed payloads and string arrays
180
+ (e.g. a `flags: string[]`). For a shape that `model:`/`many:` cannot express, use the
181
+ `deserialize: (data) => ...` escape hatch.
182
+
183
+ ### 3.6 Define `_display` on FK targets
184
+
185
+ FK and M2M dropdowns render each option via the target model's `_display`. Define it
186
+ on any model used as a relation target, or your dropdowns show nothing useful:
187
+
188
+ ```ts
189
+ public get _display(): string { return this.name; }
190
+ ```
191
+
192
+ A getter or a method both work (the library duck-types it); a **getter** is the
193
+ convention.
194
+
195
+ ---
196
+
197
+ ## 4. The collection service
198
+
199
+ One `@Injectable({providedIn: 'root'})` `Collection<T>` subclass per model. This is
200
+ rigidly uniform in practice — do not invent variations.
201
+
202
+ ```ts
203
+ @Injectable({ providedIn: 'root' })
204
+ export class ThingService extends Collection<Thing> {
205
+ constructor() {
206
+ const backend = inject(DataBackend);
207
+ super(backend, '/path/to/things', Thing);
208
+ }
209
+ }
210
+ ```
211
+
212
+ - `inject(DataBackend)` goes **inside the constructor body** — `super()` must run
213
+ before `this` exists.
214
+ - Three positional args: backend, **API-relative** path, **concrete** model class
215
+ (never the `*Base`).
216
+ - Do **not** concatenate a base URL here. It comes from `DATA_API_URL`.
217
+ - Keep the service stateless. Model-specific behaviour belongs in a separate
218
+ `*-action.service.ts`, not on the Collection subclass.
219
+
220
+ `Collection.list()` assumes a **paginated** API response and reads `result.results`.
221
+
222
+ ---
223
+
224
+ ## 5. Lists — `ModelListService`
225
+
226
+ ### 5.1 Wrap it. Do not call it from every component.
227
+
228
+ In a large production consumer, `ModelListService.get()` is called from **exactly one
229
+ place**: an abstract base `@Directive` that ~150 list components extend. If you are
230
+ adding lists to a project, look for that base class and extend it. If none exists and
231
+ you need more than one or two lists, **write one** — the per-component boilerplate is
232
+ otherwise copied everywhere.
233
+
234
+ ### 5.2 Call `get()` from `ngOnInit`, never the constructor
235
+
236
+ `get()` consumes bound `@Input()`s (`name`, `keep`, `filter`). In a constructor they
237
+ are still undefined, and you silently land in the "no name → always a fresh instance"
238
+ branch.
239
+
240
+ ```ts
241
+ private _subscriptions$ = new Subject<void>();
242
+
243
+ public ngOnInit(): void {
244
+ this.list = this._list.get<Thing>(this.name, this.coll, {
245
+ fields: this.fields,
246
+ filters: this.filters,
247
+ sorter: this.sorter,
248
+ paginator: this.paginator,
249
+ keep: this.keep,
250
+ reload: this.reload,
251
+ filter: this.filter,
252
+ unsubscribe: this._subscriptions$,
253
+ });
254
+ }
255
+
256
+ public ngOnDestroy(): void {
257
+ this._subscriptions$.next();
258
+ this._subscriptions$.complete();
259
+ }
260
+ ```
261
+
262
+ ### 5.3 `name` + `keep` is a global cache — the #1 trap
263
+
264
+ `ModelListService` holds `_instances: Map<string, ModelList>` keyed by **`name`
265
+ alone** — not by component, not by route, not by collection.
266
+
267
+ - With `keep: true`, two lists sharing a `name` **share one `ModelList`**. Field
268
+ selection, filters, sort and pagination bleed across routes.
269
+ - Worse: when a cached instance is returned, **the `defaults` you passed are silently
270
+ ignored**. Your `fields`/`filters` do nothing on the second call.
271
+
272
+ Rules: **`name` must be globally unique** (deriving it from the route is a good
273
+ habit), and use **`keep: false`** when you want a private, non-persisted list.
274
+
275
+ ### 5.4 `filter` vs `filters` — they are different things
276
+
277
+ | param | type | meaning |
278
+ |---|---|---|
279
+ | `filter` | `FilterDefaults \| Observable<FilterDefaults>` | A fixed dict **force-ANDed into every query**. Scoping/context. Not user-facing. |
280
+ | `filters` | `FiltersParams` | Definitions of the **user-facing filter widgets**. |
281
+
282
+ Mixing these up is a common and confusing mistake.
283
+
284
+ ### 5.5 Lifecycle
285
+
286
+ - **`unsubscribe`**: a `Subject<void>` you own, passed in and fired in `ngOnDestroy`.
287
+ The library `takeUntil`s it. The public API is **Subject-based, not
288
+ `DestroyRef`-based** — reuse the same Subject for your own `takeUntil` pipes rather
289
+ than mixing both mechanisms.
290
+ - **`reload`**: a `Subject<void|boolean>` you own; `.next(true)` re-runs the query.
291
+ Pass one if the list has row actions — if you don't, `get()` creates its own and
292
+ **you have no reference to it**.
293
+ - **Known defect, design around it:** `get()` only wires `reload`/`unsubscribe`
294
+ `if (!ml.started)`. Revisit a `keep: true` list that is already running and your
295
+ `reload.next(true)` does nothing. If a list needs reliable refresh, prefer
296
+ `keep: false`.
297
+ - **`fields.custom`** adds synthetic non-model columns (e.g. an actions column) so
298
+ they appear in both the table and the field selector.
299
+
300
+ ---
301
+
302
+ ## 6. Filters
303
+
304
+ ```ts
305
+ new ModelListTextFilter({ name: 'search', field: 'q', label: 'Search', help: '…' })
306
+ ```
307
+
308
+ - **`name` and `field` are different.** `field` = the query param sent to the server;
309
+ `name` = the filter's identity, used by `defaults`/`allowed`. Usually equal — but
310
+ two filters may share one `field` under different `name`s to present as distinct
311
+ widgets.
312
+ - **Always set `label` and `help` explicitly.** Every filter class hardcodes a
313
+ **French** default when you omit them. If your app is not French, omitting them
314
+ leaks French into the UI.
315
+ - Instantiate filters **inside** a `getFilters()`-style method, not as module
316
+ constants — most need an injected collection.
317
+ - Several filters **throw on missing params** (as bare strings, not `Error`s, so no
318
+ stack trace):
319
+
320
+ | filter | requires |
321
+ |---|---|
322
+ | `ModelListSelectFilter` / `…MultiFilter` | `choices` **or** `model` |
323
+ | `ModelListAutocompleteFilter` / `…MultiFilter` | `collection` **or** `queryset` |
324
+ | `ModelListTreeFilter` | `collection`/`queryset` (extends autocomplete) |
325
+ | `ModelListFlagsFilter` | `collection` |
326
+ | `ModelListGeodistanceFilter` | `geolocator` |
327
+
328
+ Notes worth knowing:
329
+
330
+ - `ModelListSelectFilter` can **reflect choices off the model** — pass `model:` (+
331
+ optional `mfield:`) instead of restating a `choices` list you already declared on
332
+ the field. `choices` also accepts an `Observable`.
333
+ - Autocomplete/tree: narrow the lookup with
334
+ `filter: { fields: ['id', 'name'].join(',') }`. This is a real performance lever —
335
+ it stops the dropdown shipping whole objects.
336
+ - `ModelListFlagsFilter` discovers its options at runtime via a collection action
337
+ (`get_flags` by default). Pass the list's own collection.
338
+ - `ModelListFilterGroup` is a **presentation container only**. Groups go into the same
339
+ `filters: []` array as ungrouped filters, and `defaults` still names **leaf filter
340
+ names**, never group names.
341
+
342
+ ---
343
+
344
+ ## 7. Display and edit — `<data-dispedit>`
345
+
346
+ ```html
347
+ <!-- a scalar, definition-list mode, auto-saves on edit -->
348
+ <data-dispedit [model]="thing" field="name" mode="dd" [editable]="true" />
349
+
350
+ <!-- an FK: [collection] is what makes the editor work -->
351
+ <data-dispedit [model]="thing" field="category" mode="dd" [editable]="true"
352
+ [collection]="categories"
353
+ [filter]="{ fields: ['id', 'name'].join(',') }" />
354
+ ```
355
+
356
+ - **`mode="dd"`** renders a `<dt>/<dd>` block that flips to an editor in place and
357
+ **auto-saves to the API**. This is the workhorse.
358
+ - **`mode="inline"` / `"form"`** renders a bare form control and **does not save** —
359
+ it expects an external `[form]` group and your own submit. **Exception:** FK/M2M
360
+ selection saves in *every* mode. The selection handler calls `_save()`
361
+ unconditionally, so an FK picked inside an `inline`/`form` form PATCHes immediately
362
+ rather than waiting for your submit. Don't design around it saving on submit.
363
+ - `field` is a **static string**, not a binding.
364
+ - **`[collection]` is required for FK/M2M editors.** `[filter]` narrows that lookup.
365
+ - The widget is chosen from field metadata. Overrides exist but are almost never the
366
+ right answer — fix the model instead.
367
+ - If you use dispedit's auto-save, render a `<data-message-zone>` somewhere;
368
+ successes and errors are reported through `DataMessageService` and are otherwise
369
+ invisible.
370
+
371
+ **Do not try to bind `[save]`.** It is declared as a plain class property, not an
372
+ `input()`, so it is not template-bindable. Control saving via `mode`.
373
+
374
+ ---
375
+
376
+ ## 8. Bootstrap wiring
377
+
378
+ ```ts
379
+ bootstrapApplication(AppComponent, {
380
+ providers: [
381
+ { provide: DATA_API_URL, useValue: environment.API_URL },
382
+
383
+ { provide: DATA_AUTH_PARAMS, useValue: {
384
+ tokenUrl: environment.API_URL + '/path/to/token',
385
+ tokenRefreshUrl: environment.API_URL + '/path/to/token/refresh',
386
+ loginField: 'username', passwordField: 'password', tokenField: 'token',
387
+ loginRoute: ['/login'], logoutRoute: ['/'],
388
+ userFetchValiditySeconds: 600,
389
+ } as AuthParams },
390
+
391
+ { provide: DATA_AUTH_URLS, useValue: [environment.OTHER_API_URL] },
392
+
393
+ // useExisting — NOT useClass. See below.
394
+ { provide: DATA_AUTH_USER_SERVICE, useExisting: UserService },
395
+ { provide: DATA_AUTH_SERVICE, useExisting: AuthService },
396
+ AuthService, // the concrete class must also be provided
397
+
398
+ provideHttpClient(withInterceptorsFromDi()),
399
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
400
+ ],
401
+ });
402
+ ```
403
+
404
+ - `DATA_API_URL` is **required** and must not have a trailing slash.
405
+ - `DATA_AUTH_SERVICE` / `DATA_AUTH_USER_SERVICE` must use **`useExisting`**, aliasing
406
+ app-owned singletons — so the concrete class must **also** be listed as a plain
407
+ provider. `useClass` there creates a **second instance** and breaks `logout$`.
408
+ - `AuthInterceptor` is a **legacy `HTTP_INTERCEPTORS` multi-provider**, which forces
409
+ `withInterceptorsFromDi()`. It is not a functional interceptor.
410
+ - **Optional, with fallbacks — provide only if you need them:**
411
+ - `DATA_DISPLAY_CONFIG` falls back to `BootstrapDataDisplayConfig` (Bootstrap 5
412
+ classes). Provide it only for a different CSS framework.
413
+ - `DATA_MAX_TRANSFERSTATE_TIME` defaults to `0` (TransferState disabled). Provide it
414
+ only for SSR.
415
+
416
+ ---
417
+
418
+ ## 9. Testing
419
+
420
+ Use the shipped mocks — **do not stand up HTTP** to test a model:
421
+
422
+ ```ts
423
+ const things = new CollectionMock<Thing>(Thing);
424
+ const t = things.fromJson({ id: 1, name: '…' }) as Thing;
425
+
426
+ things.mockValues = [t]; // drives .list()
427
+ const qs = things.queryset();
428
+ qs.mockData = [t]; // drives .get()
429
+ ```
430
+
431
+ `CollectionMock` / `QuerysetMock` are exported from the package root. A worked,
432
+ test-covered example lives in the library repo at
433
+ `projects/data/src/examples/catalog/`.
434
+
435
+ ---
436
+
437
+ ## 10. Checklist before you finish
438
+
439
+ Run through this. Every item is a silent failure, not a compile error.
440
+
441
+ - [ ] `static override readonly __name: string` on **every** class, **unique**.
442
+ - [ ] **No** `override id` anywhere.
443
+ - [ ] Every FK/M2M has a matching `<field>_details` — spelled **exactly** right.
444
+ - [ ] Every `detailsField` that should hydrate a model has `model:` (not just
445
+ `related:`).
446
+ - [ ] Every m2m id field has `defaultValue: []`.
447
+ - [ ] Every model used as an FK target defines `_display`.
448
+ - [ ] `priority` set on every field; `-1` on relation id halves.
449
+ - [ ] `ModelListService.get()` called in `ngOnInit`, with a **globally unique**
450
+ `name`, and an `unsubscribe` Subject fired in `ngOnDestroy`.
451
+ - [ ] Every filter has an explicit `label` and `help` (or you ship French).
452
+ - [ ] Collection path is API-relative; no base URL concatenated.
453
+
454
+ ---
455
+
456
+ ## Wiring this into your project
457
+
458
+ Add this to your project's own `AGENTS.md` / `CLAUDE.md` so agents pick the rules up
459
+ automatically:
460
+
461
+ ```markdown
462
+ ## @solidev/data
463
+
464
+ This project uses `@solidev/data` for its models, collections and list/edit UI.
465
+ Before writing or changing any model, collection, list component, or `dispedit`
466
+ usage, read `node_modules/@solidev/data/AGENTS.md` and follow it.
467
+
468
+ Non-negotiables from that file:
469
+ - Never declare `override id` on a model — it is inherited from `DataModel`.
470
+ - `static override readonly __name` on every model class, globally unique.
471
+ - Every foreign key needs a `<field>_details` twin; the name is a runtime contract.
472
+ - `related:` is inert documentation — `model:` on the details half does the linking.
473
+ - `ModelListService.get()` goes in `ngOnInit`, with a globally unique `name`.
474
+ - Always set `label`/`help` on filters, or French defaults leak into the UI.
475
+ ```
package/README.md CHANGED
@@ -1,7 +1,97 @@
1
- # @solidev/data library
1
+ # @solidev/data
2
2
 
3
- - data fields / models / collections
4
- - data backends & query
5
- - model list display
6
- - model edit
3
+ A **Django-REST-Framework-style data layer for Angular**. Declare a model once and get
4
+ HTTP access, querysets, filtering, sorting, pagination, forms, validation and
5
+ display/edit widgets all driven by that single declaration.
7
6
 
7
+ ```ts
8
+ export class ProductBase extends DataModel {
9
+ static override readonly __name: string = 'ProductBase';
10
+
11
+ @charField({ description: 'Name', maxLength: 200 })
12
+ public name!: string;
13
+
14
+ @charField({ description: 'Status', choices: [
15
+ { value: 'draft', desc: 'Draft' },
16
+ { value: 'published', desc: 'Published' },
17
+ ]})
18
+ public status!: string;
19
+
20
+ @foreignKeyField({ description: 'Category', related: 'Category', priority: -1 })
21
+ public category!: number;
22
+ }
23
+ ```
24
+
25
+ ```html
26
+ <data-dispedit [model]="product" field="name" mode="dd" [editable]="true" />
27
+ <data-dispedit [model]="product" field="status" mode="dd" [editable]="true" />
28
+ <data-dispedit [model]="product" field="category" mode="dd" [editable]="true"
29
+ [collection]="categories" />
30
+ ```
31
+
32
+ That is the whole idea: `status` renders as a `<select>` and `category` as an FK
33
+ dropdown **because the field metadata says so**. You declare the model; the UI follows.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ npm install @solidev/data
39
+ ```
40
+
41
+ ```ts
42
+ bootstrapApplication(AppComponent, {
43
+ providers: [
44
+ { provide: DATA_API_URL, useValue: 'https://api.example.com/v1' },
45
+ provideHttpClient(),
46
+ ],
47
+ });
48
+ ```
49
+
50
+ ## What's in the box
51
+
52
+ | Module | What it gives you |
53
+ |---|---|
54
+ | **data** | `DataModel` + field decorators, `Collection<T>`, `Queryset<T>`, `DataBackend` (with SSR `TransferState` support) |
55
+ | **modellist** | `ModelListService` — field selection, filters, sorter, paginator, and the components to render them |
56
+ | **dispedit** | `<data-dispedit>` and friends — metadata-driven display/edit, FK/M2M pickers, flags, safe-delete |
57
+ | **auth** | JWT service + interceptor with automatic refresh and replay |
58
+ | **messages, routing, updates, uploader, richedit** | supporting pieces |
59
+
60
+ ## Documentation
61
+
62
+ - **Guides** — see the `docs/` directory in the repository, also published in the
63
+ generated documentation under **Guides**.
64
+ - **API reference** — generated with [compodoc](https://compodoc.app):
65
+ `npm run mkdocs:serve`.
66
+ - **Worked example** — `projects/data/src/examples/catalog/`, a small catalog domain
67
+ (tree categories, m2m tags, geolocated stores, flags) that is **test-covered**, so it
68
+ cannot drift from the library.
69
+
70
+ ### Using this library with an AI agent
71
+
72
+ This package ships **[`AGENTS.md`](./AGENTS.md)** — a rules file covering the model
73
+ conventions and the runtime contracts that fail silently when broken. It is available
74
+ in consuming projects at `node_modules/@solidev/data/AGENTS.md`.
75
+
76
+ Point your agent at it from your own `AGENTS.md` / `CLAUDE.md`:
77
+
78
+ ```markdown
79
+ This project uses `@solidev/data`. Before writing or changing any model, collection,
80
+ list component, or `dispedit` usage, read `node_modules/@solidev/data/AGENTS.md`
81
+ and follow it.
82
+ ```
83
+
84
+ ## Start here
85
+
86
+ Whatever else you skip, read the **Relations** guide and the **Traps** guide. The
87
+ FK ⇄ details pairing is a runtime contract enforced by string concatenation, and most
88
+ of the ways to misuse this library fail **silently**.
89
+
90
+ ## Requirements
91
+
92
+ Angular 22+. Peer dependencies: `@angular/common`, `@angular/core`, and — depending on
93
+ which parts you use — `@ng-bootstrap/ng-bootstrap`, `jwt-decode`, `ngx-editor`.
94
+
95
+ ## Licence
96
+
97
+ See [LICENSE](../../LICENSE).