@solidev/data 1.0.0-beta.1 → 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/AGENTS.md +475 -0
- package/README.md +95 -5
- package/fesm2022/solidev-data-richedit.mjs +82 -8
- package/fesm2022/solidev-data-richedit.mjs.map +1 -1
- package/fesm2022/solidev-data.mjs +4370 -352
- package/fesm2022/solidev-data.mjs.map +1 -1
- package/package.json +1 -1
- package/types/solidev-data-richedit.d.ts +60 -5
- package/types/solidev-data.d.ts +4125 -205
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
|
|
1
|
+
# @solidev/data
|
|
2
2
|
|
|
3
|
-
- data
|
|
4
|
-
|
|
5
|
-
|
|
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).
|