orange-dragonfly-model 0.11.5 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +511 -5
- package/dist/index.cjs +373 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +342 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -15
- package/.eslintrc.json +0 -18
- package/components/model.js +0 -333
- package/components/validation-exception.js +0 -11
- package/index.js +0 -3
- package/tests/lookup-query.test.js +0 -50
- package/tests/output.test.js +0 -58
- package/tests/test-model-schema.json +0 -19
- package/tests/test-model.js +0 -31
package/README.md
CHANGED
|
@@ -1,5 +1,511 @@
|
|
|
1
|
-
# Orange Dragonfly Model
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# Orange Dragonfly Model
|
|
2
|
+
|
|
3
|
+
An active-record model class for the Orange Dragonfly framework. Extends [`orange-dragonfly-orm`](https://github.com/charger88/orange-dragonfly-orm)'s `ActiveRecord` with validation, access control, field restrictions, and structured output serialisation.
|
|
4
|
+
|
|
5
|
+
## Table of contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Quick start](#quick-start)
|
|
9
|
+
- [Defining a model](#defining-a-model)
|
|
10
|
+
- [Field restrictions](#field-restrictions)
|
|
11
|
+
- [CRUD operations](#crud-operations)
|
|
12
|
+
- [Querying](#querying)
|
|
13
|
+
- [Validation](#validation)
|
|
14
|
+
- [Access control](#access-control)
|
|
15
|
+
- [Output serialisation](#output-serialisation)
|
|
16
|
+
- [Error handling](#error-handling)
|
|
17
|
+
- [Deprecated API](#deprecated-api)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install orange-dragonfly-model
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`orange-dragonfly-orm` and `orange-dragonfly-validator` are regular dependencies and are installed automatically.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Model } from 'orange-dragonfly-model'
|
|
35
|
+
import type { ODValidatorRulesSchema } from 'orange-dragonfly-validator'
|
|
36
|
+
import { Relation } from 'orange-dragonfly-orm'
|
|
37
|
+
|
|
38
|
+
class Article extends Model {
|
|
39
|
+
static override get validation_rules(): ODValidatorRulesSchema {
|
|
40
|
+
return {
|
|
41
|
+
id: { required: false, type: 'integer', min: 1 },
|
|
42
|
+
title: { required: true, type: 'string', min: 1, max: 255 },
|
|
43
|
+
body: { required: true, type: 'string', min: 1 },
|
|
44
|
+
author_id: { required: true, type: 'integer', min: 1 },
|
|
45
|
+
published: { required: false, type: 'boolean' },
|
|
46
|
+
created_at: { required: false, type: 'integer' },
|
|
47
|
+
updated_at: { required: false, type: 'integer' },
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static override get unique_keys(): string[][] {
|
|
52
|
+
return [['title']]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static override get available_relations() {
|
|
56
|
+
return {
|
|
57
|
+
author: Relation.parent(this, User),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override get output(): Record<string, unknown> {
|
|
62
|
+
return {
|
|
63
|
+
id: this.id,
|
|
64
|
+
title: this.data.title,
|
|
65
|
+
published: this.data.published,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create
|
|
71
|
+
const article = await Article.create({ title: 'Hello', body: 'World', author_id: 1 })
|
|
72
|
+
|
|
73
|
+
// Update
|
|
74
|
+
await article.update({ title: 'Hello World' })
|
|
75
|
+
|
|
76
|
+
// Lookup
|
|
77
|
+
const q = Article.lookupQuery({ title: 'Hello World' })
|
|
78
|
+
const results = await q.select()
|
|
79
|
+
|
|
80
|
+
// Serialise with relations
|
|
81
|
+
const json = await article.getExtendedOutput(['author'])
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Defining a model
|
|
87
|
+
|
|
88
|
+
Subclass `Model` and override the static getters that describe the model's shape and behaviour. Every getter is optional — the base class provides safe defaults.
|
|
89
|
+
|
|
90
|
+
### validation_rules
|
|
91
|
+
|
|
92
|
+
Returns an [`orange-dragonfly-validator`](https://github.com/charger88/orange-dragonfly-validator) schema that describes every field the model accepts. This schema is used by `validate()`, `create()`, `update()`, and `lookupQuery()`.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
static override get validation_rules(): ODValidatorRulesSchema {
|
|
96
|
+
return {
|
|
97
|
+
id: { required: false, type: 'integer', min: 1 },
|
|
98
|
+
username: { required: true, type: 'string', min: 1, max: 64 },
|
|
99
|
+
email: { required: true, type: 'string', min: 5, max: 255 },
|
|
100
|
+
active: { required: false, type: 'boolean' },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The default rule set contains only `id`.
|
|
106
|
+
|
|
107
|
+
**Boolean coercion.** During validation, fields declared as `type: 'boolean'` are automatically coerced: the integer `1` becomes `true` and `0` becomes `false`. This handles databases that store booleans as integers.
|
|
108
|
+
|
|
109
|
+
**Relation integrity.** For every `parent`-mode relation defined in `available_relations`, `validate()` checks that the referenced parent record exists whenever the foreign-key field is present and non-null/non-zero.
|
|
110
|
+
|
|
111
|
+
### special_fields
|
|
112
|
+
|
|
113
|
+
Derived automatically from `validation_rules` — no override needed. A field is "special" when its name is one of `created_at`, `updated_at`, or `deleted_at`. Special fields are managed by `ActiveRecord.save()` and `ActiveRecord.remove()` and are automatically added to `restricted_for_create` and `restricted_for_update`.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// If your rules include created_at and updated_at:
|
|
117
|
+
static override get validation_rules(): ODValidatorRulesSchema {
|
|
118
|
+
return {
|
|
119
|
+
id: { required: false, type: 'integer', min: 1 },
|
|
120
|
+
name: { required: true, type: 'string' },
|
|
121
|
+
created_at: { required: false, type: 'integer' },
|
|
122
|
+
updated_at: { required: false, type: 'integer' },
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// → special_fields returns ['created_at', 'updated_at'] automatically
|
|
126
|
+
// → restricted_for_create returns ['id', 'created_at', 'updated_at'] automatically
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### unique_keys
|
|
130
|
+
|
|
131
|
+
A list of field-name groups that must be unique across the table. Each inner array is one composite key. Checked automatically on every `save()`.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
static override get unique_keys(): string[][] {
|
|
135
|
+
return [
|
|
136
|
+
['email'], // email alone must be unique
|
|
137
|
+
['first_name', 'last_name'], // combination must be unique
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
When a uniqueness violation is detected during save, an `OrangeDatabaseInputValidationError` is thrown with every field in the failing key listed in `.info`.
|
|
143
|
+
|
|
144
|
+
**Null handling.** By default, null values in a key cause the uniqueness check to pass (treating null as always-unique). This matches SQL `UNIQUE` index behaviour where `NULL ≠ NULL`. You can control this per-call via the `ignore_null` parameter of `checkUniqueness()`.
|
|
145
|
+
|
|
146
|
+
### fulltext_indexes
|
|
147
|
+
|
|
148
|
+
Declares the fulltext indexes that exist on the table. This is informational metadata — the library itself does not automatically build MATCH/AGAINST queries from it, but your query-builder layer or framework tooling can inspect it.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
static override get fulltext_indexes(): string[][] {
|
|
152
|
+
return [
|
|
153
|
+
['title', 'body'], // composite fulltext index
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### ignore_extra_fields
|
|
159
|
+
|
|
160
|
+
When `true`, fields present in input data that are not declared in `validation_rules` are silently dropped instead of raising an error. Applies to `create()`, `update()`, `lookupQuery()`, and `_preSave()`.
|
|
161
|
+
|
|
162
|
+
Useful when the application shares a database with other systems and columns may be added to a table outside of your control.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
static override get ignore_extra_fields(): boolean {
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Default: `false` — unknown fields throw `OrangeDatabaseInputValidationError`.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Field restrictions
|
|
175
|
+
|
|
176
|
+
These getters return an array of field names that are blocked in specific operations. Attempting to pass a restricted field throws `OrangeDatabaseInputValidationError`.
|
|
177
|
+
|
|
178
|
+
### restricted_for_lookup
|
|
179
|
+
|
|
180
|
+
Fields that callers may not filter by in `lookupQuery()`.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
static override get restricted_for_lookup(): string[] {
|
|
184
|
+
return ['password_hash', 'secret_token']
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Default: `[]`.
|
|
189
|
+
|
|
190
|
+
### restricted_for_create
|
|
191
|
+
|
|
192
|
+
Fields that callers may not supply to `create()`. Defaults to `['id', ...special_fields]` — override to extend it.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
static override get restricted_for_create(): string[] {
|
|
196
|
+
return super.restricted_for_create.concat(['role']) // callers cannot set role on creation
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### restricted_for_update
|
|
201
|
+
|
|
202
|
+
Fields that callers may not supply to `update()`. Defaults to `['id', ...special_fields]`.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
static override get restricted_for_update(): string[] {
|
|
206
|
+
return super.restricted_for_update.concat(['email']) // email cannot be changed after creation
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### restricted_for_output
|
|
211
|
+
|
|
212
|
+
Relation names that `getExtendedOutput()` will refuse to embed. Trying to include a restricted relation throws a plain `Error`.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
static override get restricted_for_output(): string[] {
|
|
216
|
+
return ['payment_details']
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Default: `[]`.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## CRUD operations
|
|
225
|
+
|
|
226
|
+
### create
|
|
227
|
+
|
|
228
|
+
Validates field names and restrictions, then inserts a new record. Validation rules and uniqueness constraints are enforced by `_preSave()` before the DB write.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
const user = await User.create({
|
|
232
|
+
username: 'alice',
|
|
233
|
+
email: 'alice@example.com',
|
|
234
|
+
})
|
|
235
|
+
// → User instance with id set by the database
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Throws `OrangeDatabaseInputValidationError` when:
|
|
239
|
+
- a field is not in `validation_rules` (and `ignore_extra_fields` is `false`)
|
|
240
|
+
- a field is in `restricted_for_create`
|
|
241
|
+
- validation fails (type mismatch, required field missing, uniqueness violation, etc.)
|
|
242
|
+
|
|
243
|
+
### update
|
|
244
|
+
|
|
245
|
+
Merges the supplied fields into the record and saves it. The instance must already have an `id` (i.e. it was loaded from the database or returned by `create()`).
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
await article.update({ title: 'Revised title', body: 'Updated content' })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Throws `Error('You can update saved object only')` if `this.id` is falsy. Throws `OrangeDatabaseInputValidationError` for the same field-level reasons as `create()`.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Querying
|
|
256
|
+
|
|
257
|
+
### lookupQuery
|
|
258
|
+
|
|
259
|
+
Builds a `SelectQuery` from a plain data object, applying field validation and restriction checks before any SQL is generated. Returns the query so you can chain further conditions or execute it.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// Simple equality
|
|
263
|
+
const q = User.lookupQuery({ username: 'alice' })
|
|
264
|
+
const users = await q.select()
|
|
265
|
+
|
|
266
|
+
// Array → IN (...)
|
|
267
|
+
const q2 = User.lookupQuery({ id: [1, 2, 3] })
|
|
268
|
+
|
|
269
|
+
// Compose with an existing query (e.g. add ORDER BY before calling select)
|
|
270
|
+
const base = User.selectQuery().orderBy('created_at', 'DESC')
|
|
271
|
+
const q3 = User.lookupQuery({ active: true }, base)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Fields not present in `validation_rules` throw unless `ignore_extra_fields` is `true`. Fields in `restricted_for_lookup` always throw.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Validation
|
|
279
|
+
|
|
280
|
+
### validate
|
|
281
|
+
|
|
282
|
+
Called automatically by `_preSave()` on every `create()` / `update()` / `save()`. Can also be called manually.
|
|
283
|
+
|
|
284
|
+
Performs three checks in order:
|
|
285
|
+
|
|
286
|
+
1. **Schema validation** — runs `orange-dragonfly-validator` `parse()` against `this.data` using `validation_rules`.
|
|
287
|
+
2. **Custom validation** — calls `custom_validation()` and collects any returned errors.
|
|
288
|
+
3. **Relation integrity** — for each `parent`-mode relation, if the foreign-key field is set to a non-null, non-zero value, verifies the parent record exists.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const user = new User({ username: '', email: 'not-an-email' })
|
|
292
|
+
await user.validate() // throws OrangeDatabaseInputValidationError
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### custom_validation
|
|
296
|
+
|
|
297
|
+
Override to add application-specific validation logic. Return `null` (or an empty object) on success, or a `Record<string, string>` mapping field names to error messages.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
override async custom_validation(): Promise<Record<string, string> | null> {
|
|
301
|
+
if (this.data.password !== this.data.password_confirm) {
|
|
302
|
+
return { password_confirm: 'Passwords do not match' }
|
|
303
|
+
}
|
|
304
|
+
if (await User.selectQuery().where('email', this.data.email).exists()) {
|
|
305
|
+
return { email: 'Email is already taken' }
|
|
306
|
+
}
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### checkUniqueness
|
|
312
|
+
|
|
313
|
+
Checks all `unique_keys` constraints against the database. Called automatically during `_preSave()` with `exception_mode = true` and `ignore_null = true`. Can be called manually for pre-flight checks.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// Returns boolean
|
|
317
|
+
const isOk = await article.checkUniqueness()
|
|
318
|
+
|
|
319
|
+
// Throws OrangeDatabaseInputValidationError if not unique
|
|
320
|
+
await article.checkUniqueness(true)
|
|
321
|
+
|
|
322
|
+
// Treat null values as non-unique (strict mode)
|
|
323
|
+
await article.checkUniqueness(true, false)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| Parameter | Default | Description |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `exception_mode` | `false` | Throw instead of returning `false` when a violation is found |
|
|
329
|
+
| `ignore_null` | `false` | Return `true` immediately when any key field is `null` (null is always-unique) |
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Access control
|
|
334
|
+
|
|
335
|
+
### accessible
|
|
336
|
+
|
|
337
|
+
Override to implement per-object access control. Receives the requesting user and an optional mode string. Returns `true` when access should be granted.
|
|
338
|
+
|
|
339
|
+
The base implementation grants access when `mode` is `null` and denies it otherwise — a safe default that lets you add access checks incrementally.
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
override async accessible(user: AuthUser | null, mode: string | null = null): Promise<boolean> {
|
|
343
|
+
if (!user) return false
|
|
344
|
+
if (user.role === 'admin') return true
|
|
345
|
+
if (mode === 'write') return this.data.owner_id === user.id
|
|
346
|
+
return true // read access for any authenticated user
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### findAndCheckAccessOrDie
|
|
351
|
+
|
|
352
|
+
Fetches a record by id and calls `accessible()`. Throws a plain `Error` if the record is not found or not accessible — suitable for use in API route handlers.
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// In a route handler:
|
|
356
|
+
const article = await Article.findAndCheckAccessOrDie(req.params.id, req.user, 'write')
|
|
357
|
+
// Continues only if the record exists and accessible(user, 'write') returns true
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Error messages:
|
|
361
|
+
- `"Article #42 not found"` — record missing
|
|
362
|
+
- `"Article #42 is not accessible"` — mode is `null`
|
|
363
|
+
- `"Article #42 is not accessible for write"` — mode is provided
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Output serialisation
|
|
368
|
+
|
|
369
|
+
### output
|
|
370
|
+
|
|
371
|
+
A getter that returns the public representation of the record. Override to expose exactly the fields you want — omit sensitive data and include any computed values.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
override get output(): Record<string, unknown> {
|
|
375
|
+
return {
|
|
376
|
+
id: this.id,
|
|
377
|
+
username: this.data.username,
|
|
378
|
+
joined_at: this.data.created_at,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The base implementation returns `{ id: this.id }`.
|
|
384
|
+
|
|
385
|
+
### formatOutput
|
|
386
|
+
|
|
387
|
+
Called by `getExtendedOutput()`. Override when the output shape depends on the access context (e.g. return fewer fields for relation embeds).
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
override formatOutput(mode: string | null = null): Record<string, unknown> {
|
|
391
|
+
const base = this.output
|
|
392
|
+
if (mode?.startsWith('relation:')) {
|
|
393
|
+
// Return a compact representation when embedded as a relation
|
|
394
|
+
return { id: base.id, username: base.username }
|
|
395
|
+
}
|
|
396
|
+
return base
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The base implementation delegates to `output` regardless of `mode`.
|
|
401
|
+
|
|
402
|
+
### getExtendedOutput
|
|
403
|
+
|
|
404
|
+
Embeds relation data alongside the base output. Pass a flat list of relation names; use colon-separated paths for nesting.
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// Embed a single relation
|
|
408
|
+
const data = await article.getExtendedOutput(['author'])
|
|
409
|
+
// → { id, title, ..., ':author': { id, username } }
|
|
410
|
+
|
|
411
|
+
// Nest relations: embed author's profile inside author
|
|
412
|
+
const data = await article.getExtendedOutput(['author', 'author:profile'])
|
|
413
|
+
// → { ..., ':author': { ..., ':profile': { ... } } }
|
|
414
|
+
|
|
415
|
+
// Array relations work automatically
|
|
416
|
+
const data = await post.getExtendedOutput(['comments'])
|
|
417
|
+
// → { ..., ':comments': [{ ... }, { ... }] }
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Relation keys in the returned object are prefixed with `:` to distinguish them from plain fields. Relations listed in `restricted_for_output` throw `OrangeDatabaseModelError` when requested.
|
|
421
|
+
|
|
422
|
+
The `mode` string is threaded through `formatOutput()` calls on embedded objects as `"relation:ParentModel.relationName"`, which lets models adjust their output when they are rendered as embedded relations.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Error handling
|
|
427
|
+
|
|
428
|
+
The library uses a typed error hierarchy, all rooted in `OrangeDatabaseError` from `orange-dragonfly-orm`:
|
|
429
|
+
|
|
430
|
+
```
|
|
431
|
+
OrangeDatabaseError (orange-dragonfly-orm)
|
|
432
|
+
├── OrangeDatabaseInputError (orange-dragonfly-orm)
|
|
433
|
+
│ └── OrangeDatabaseInputValidationError field-level validation failures
|
|
434
|
+
└── OrangeDatabaseModelError model configuration / programming errors
|
|
435
|
+
└── OrangeDatabaseModelRuntimeError runtime state errors
|
|
436
|
+
└── OrangeDatabaseModelAccessError access-control failures
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
All classes are exported from `orange-dragonfly-model`.
|
|
440
|
+
|
|
441
|
+
### OrangeDatabaseInputValidationError
|
|
442
|
+
|
|
443
|
+
Thrown by `create()`, `update()`, `lookupQuery()`, `validate()`, and `checkUniqueness()` whenever field-level input is invalid. Carries an `info` map of per-field error messages.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { OrangeDatabaseInputValidationError } from 'orange-dragonfly-model'
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
await User.create({ username: '', email: 'bad' })
|
|
450
|
+
} catch (e) {
|
|
451
|
+
if (e instanceof OrangeDatabaseInputValidationError) {
|
|
452
|
+
console.log(e.message) // 'Validation failed'
|
|
453
|
+
console.log(e.info) // { username: '...', email: '...' }
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### OrangeDatabaseModelError
|
|
459
|
+
|
|
460
|
+
Thrown for model configuration or programming errors — for example, calling `getExtendedOutput()` with a relation that is in `restricted_for_output`, or accessing a model whose `validation_rules` returns `null`.
|
|
461
|
+
|
|
462
|
+
### OrangeDatabaseModelRuntimeError
|
|
463
|
+
|
|
464
|
+
Thrown for runtime state violations — for example, calling `update()` on an unsaved instance, or `findAndCheckAccessOrDie()` when the requested record does not exist.
|
|
465
|
+
|
|
466
|
+
### OrangeDatabaseModelAccessError
|
|
467
|
+
|
|
468
|
+
Thrown by `findAndCheckAccessOrDie()` when `accessible()` returns `false`. Extends `OrangeDatabaseModelRuntimeError`, so a single `catch (e instanceof OrangeDatabaseModelRuntimeError)` covers both not-found and access-denied cases if needed.
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import {
|
|
472
|
+
OrangeDatabaseModelRuntimeError,
|
|
473
|
+
OrangeDatabaseModelAccessError,
|
|
474
|
+
} from 'orange-dragonfly-model'
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const article = await Article.findAndCheckAccessOrDie(id, user, 'write')
|
|
478
|
+
} catch (e) {
|
|
479
|
+
if (e instanceof OrangeDatabaseModelAccessError) {
|
|
480
|
+
// 403 — record exists but user cannot access it
|
|
481
|
+
} else if (e instanceof OrangeDatabaseModelRuntimeError) {
|
|
482
|
+
// 404 — record not found
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Deprecated API
|
|
490
|
+
|
|
491
|
+
The following uppercase getters were renamed for naming consistency. They still work as proxies but will be removed in a future major version.
|
|
492
|
+
|
|
493
|
+
| Deprecated | Replacement |
|
|
494
|
+
|---|---|
|
|
495
|
+
| `IGNORE_EXTRA_FIELDS` | `ignore_extra_fields` |
|
|
496
|
+
| `UNIQUE_KEYS` | `unique_keys` |
|
|
497
|
+
| `FULLTEXT_INDEXES` | `fulltext_indexes` |
|
|
498
|
+
|
|
499
|
+
Migrate by renaming your overrides:
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
// Before
|
|
503
|
+
static override get UNIQUE_KEYS(): string[][] {
|
|
504
|
+
return [['email']]
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// After
|
|
508
|
+
static override get unique_keys(): string[][] {
|
|
509
|
+
return [['email']]
|
|
510
|
+
}
|
|
511
|
+
```
|