@nest-admin/nestjs 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/admin-ui/assets/index-AyWOamlt.js +50 -0
- package/dist/admin-ui/assets/index-AyWOamlt.js.map +1 -0
- package/dist/admin-ui/assets/index-D4Eh84eD.css +2 -0
- package/dist/admin-ui/index.html +14 -0
- package/dist/chunk-7IXLRGGQ.js +356 -0
- package/dist/chunk-7IXLRGGQ.js.map +1 -0
- package/dist/drizzle.cjs +895 -0
- package/dist/drizzle.cjs.map +1 -0
- package/dist/drizzle.d.cts +335 -0
- package/dist/drizzle.d.ts +335 -0
- package/dist/drizzle.js +756 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/index.cjs +3247 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1652 -0
- package/dist/index.d.ts +1652 -0
- package/dist/index.js +2901 -0
- package/dist/index.js.map +1 -0
- package/dist/prisma.cjs +1159 -0
- package/dist/prisma.cjs.map +1 -0
- package/dist/prisma.d.cts +585 -0
- package/dist/prisma.d.ts +585 -0
- package/dist/prisma.js +995 -0
- package/dist/prisma.js.map +1 -0
- package/package.json +130 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1652 @@
|
|
|
1
|
+
import { ExecutionContext, DynamicModule, ModuleMetadata, FactoryProvider, Type } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalised, ORM-independent description of a model and its fields.
|
|
5
|
+
*
|
|
6
|
+
* Every ORM adapter translates its own schema representation (Prisma DMMF,
|
|
7
|
+
* TypeORM entity metadata, a Drizzle table object, ...) into these shapes.
|
|
8
|
+
* Nothing downstream - the CRUD engine, the HTTP API, the admin UI - is
|
|
9
|
+
* allowed to look at anything else.
|
|
10
|
+
*
|
|
11
|
+
* @experimental Draft contract. Expected to change during MVP implementation.
|
|
12
|
+
*/
|
|
13
|
+
/** ORM-independent classification of a scalar or relation field. */
|
|
14
|
+
type FieldKind = 'string' | 'number' | 'boolean' | 'datetime' | 'enum' | 'json' | 'relation'
|
|
15
|
+
/** The adapter recognised the field but cannot map it onto a known kind. */
|
|
16
|
+
| 'unknown';
|
|
17
|
+
/** Cardinality of a relation from the owning model's point of view. */
|
|
18
|
+
type RelationCardinality = 'one' | 'many';
|
|
19
|
+
/**
|
|
20
|
+
* A relation, and how to act on it.
|
|
21
|
+
*
|
|
22
|
+
* `from` and `to` are what turn a relation from something an admin can only
|
|
23
|
+
* display into something it can filter and write. A to-one relation is stored
|
|
24
|
+
* as an ordinary scalar column - `Post.authorId` - and that column is what a
|
|
25
|
+
* query has to be expressed in terms of. Without knowing its name, a filter on
|
|
26
|
+
* `author` cannot be translated, and a form has no field to submit.
|
|
27
|
+
*
|
|
28
|
+
* Both are absent on to-many relations, which have no column on this side.
|
|
29
|
+
*/
|
|
30
|
+
interface RelationMetadata {
|
|
31
|
+
/** `name` of the {@link ModelMetadata} on the other side of the relation. */
|
|
32
|
+
readonly targetModel: string;
|
|
33
|
+
readonly cardinality: RelationCardinality;
|
|
34
|
+
/**
|
|
35
|
+
* Scalar field on **this** model holding the foreign key, for a to-one
|
|
36
|
+
* relation - `authorId` on `Post.author`.
|
|
37
|
+
*
|
|
38
|
+
* Absent when the relation has no column on this side: every to-many, and
|
|
39
|
+
* the non-owning half of a one-to-one.
|
|
40
|
+
*/
|
|
41
|
+
readonly from?: string;
|
|
42
|
+
/** Field on the target model that `from` points at - usually its id. */
|
|
43
|
+
readonly to?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Name shared by both halves of the relation.
|
|
46
|
+
*
|
|
47
|
+
* The only reliable way to pair `User.posts` with `Post.author`, which two
|
|
48
|
+
* things need. Distinguishing a many-to-many from a one-to-many requires
|
|
49
|
+
* looking at the other side - both are `'many'` from here, but only one has
|
|
50
|
+
* no column anywhere. And knowing whether a child's key is required decides
|
|
51
|
+
* whether it can be detached at all.
|
|
52
|
+
*
|
|
53
|
+
* Two relations between the same pair of models are told apart by it too:
|
|
54
|
+
* `Post.author` and `Post.reviewer` both target `User`.
|
|
55
|
+
*/
|
|
56
|
+
readonly name?: string;
|
|
57
|
+
}
|
|
58
|
+
interface FieldMetadata {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly kind: FieldKind;
|
|
61
|
+
/** Part of the model's primary key. */
|
|
62
|
+
readonly isId: boolean;
|
|
63
|
+
readonly isRequired: boolean;
|
|
64
|
+
readonly isUnique: boolean;
|
|
65
|
+
/** The field holds a list of {@link FieldKind} values. */
|
|
66
|
+
readonly isList: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The value is produced by the database or the ORM and is not asked of the
|
|
69
|
+
* user - `@default(cuid())`, `@default(now())`, `@default(autoincrement())`,
|
|
70
|
+
* `@updatedAt`. Such fields are displayed but not editable.
|
|
71
|
+
*
|
|
72
|
+
* This is NOT "has a default". A field with a literal default
|
|
73
|
+
* (`active Boolean @default(true)`) is an ordinary editable field that
|
|
74
|
+
* happens to arrive pre-filled; see {@link FieldMetadata.defaultValue}.
|
|
75
|
+
*
|
|
76
|
+
* NAME COLLISION - read before implementing an adapter. Prisma's DMMF also
|
|
77
|
+
* has a field called `isGenerated`, and it does NOT mean this. Measured
|
|
78
|
+
* against Prisma 7.10.0, DMMF reports `isGenerated: false` for
|
|
79
|
+
* `id String @id @default(cuid())`. Mapping it across directly produces
|
|
80
|
+
* editable primary keys. The correct derivation - a *function* default, or
|
|
81
|
+
* an updated-at column - is in `packages/prisma/src/metadata/to-metadata.ts`
|
|
82
|
+
* and `packages/drizzle/src/metadata/to-metadata.ts`, which state it in each
|
|
83
|
+
* ORM's own terms.
|
|
84
|
+
*/
|
|
85
|
+
readonly isGenerated: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Accepted on a write, never returned on a read.
|
|
88
|
+
*
|
|
89
|
+
* Set by `writeOnly` in the configuration. A password is the reason it
|
|
90
|
+
* exists: it has to be typed into a form and must never come back out, and
|
|
91
|
+
* `hidden` cannot express that - it refuses the field in both directions, so
|
|
92
|
+
* a hidden password column leaves no way to set one.
|
|
93
|
+
*
|
|
94
|
+
* Enforced twice, deliberately: the field is left out of the columns the
|
|
95
|
+
* adapter is asked for, *and* out of the projection applied to whatever comes
|
|
96
|
+
* back. One of those is enough; two is what it takes for a future adapter
|
|
97
|
+
* that ignores the field scope not to become a leak.
|
|
98
|
+
*/
|
|
99
|
+
readonly writeOnly?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Literal default the admin should pre-fill on create, when the schema
|
|
102
|
+
* declares one (`@default(true)`, `@default(0)`, `@default("USER")`).
|
|
103
|
+
*
|
|
104
|
+
* Absent for generated values: there is no literal to pre-fill for
|
|
105
|
+
* `@default(now())`, and {@link FieldMetadata.isGenerated} is `true` instead.
|
|
106
|
+
*/
|
|
107
|
+
readonly defaultValue?: unknown;
|
|
108
|
+
/** Populated when `kind` is `'enum'`. */
|
|
109
|
+
readonly enumValues?: readonly string[];
|
|
110
|
+
/** Populated when `kind` is `'relation'`. */
|
|
111
|
+
readonly relation?: RelationMetadata;
|
|
112
|
+
}
|
|
113
|
+
interface ModelMetadata {
|
|
114
|
+
/** Adapter-facing identifier, e.g. the Prisma model name `User`. */
|
|
115
|
+
readonly name: string;
|
|
116
|
+
/**
|
|
117
|
+
* Field names forming the primary key. Modelled as a list rather than a
|
|
118
|
+
* single `id` so composite keys do not require a breaking change later,
|
|
119
|
+
* even though the MVP will only support single-column keys.
|
|
120
|
+
*/
|
|
121
|
+
readonly primaryKey: readonly string[];
|
|
122
|
+
readonly fields: readonly FieldMetadata[];
|
|
123
|
+
/**
|
|
124
|
+
* Field that names a record of this model in one line, when the application
|
|
125
|
+
* has declared one.
|
|
126
|
+
*
|
|
127
|
+
* A slot rather than a value: left unset, `displayFieldFor` works it out from
|
|
128
|
+
* the fields. It is here so that a declared choice travels with the model and
|
|
129
|
+
* reaches the adapter and the metadata document alike, without either of them
|
|
130
|
+
* having to read configuration.
|
|
131
|
+
*/
|
|
132
|
+
readonly displayField?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Per-model and per-field configuration.
|
|
137
|
+
*
|
|
138
|
+
* The schema says what a model *is*; this says how the admin should treat it.
|
|
139
|
+
* Two different questions, so they are two different inputs - a column being a
|
|
140
|
+
* string is a fact about the database, and that column being a password is a
|
|
141
|
+
* fact about the application.
|
|
142
|
+
*
|
|
143
|
+
* The overrides divide into two kinds, and the difference matters:
|
|
144
|
+
*
|
|
145
|
+
* behaviour `hidden`, `readOnly`, `displayField`. Enforced. A hidden
|
|
146
|
+
* field is removed from the metadata every layer reads, so it
|
|
147
|
+
* cannot be filtered, sorted, written or returned - see
|
|
148
|
+
* `applyOverrides`.
|
|
149
|
+
*
|
|
150
|
+
* behaviour `writeOnly` too - accepted on a write and stripped from
|
|
151
|
+
* every read.
|
|
152
|
+
*
|
|
153
|
+
* presentation `label`, `widget`, `order`. Passed to the client, which is
|
|
154
|
+
* free to ignore them. Nothing depends on them being honoured.
|
|
155
|
+
*
|
|
156
|
+
* Anything in the first group that were only presentation would be a security
|
|
157
|
+
* hole with a reassuring name.
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* How a field should be edited, when its type does not say enough.
|
|
162
|
+
*
|
|
163
|
+
* A `string` column may be a sentence, a password, an address or a colour, and
|
|
164
|
+
* the schema cannot tell them apart. Deliberately a closed list: a client has
|
|
165
|
+
* to know how to render each one, so an open string would mean silently
|
|
166
|
+
* falling back to a plain input and no way to notice.
|
|
167
|
+
*/
|
|
168
|
+
type FieldWidget = 'textarea' | 'password' | 'email' | 'url' | 'color' | 'json';
|
|
169
|
+
interface FieldOverride {
|
|
170
|
+
/**
|
|
171
|
+
* Remove the field from the admin entirely.
|
|
172
|
+
*
|
|
173
|
+
* **Enforced, not cosmetic.** The field is dropped from the metadata before
|
|
174
|
+
* anything reads it, so it is absent from the schema document, rejected in
|
|
175
|
+
* filters and sorts, refused in writes, and stripped from every response.
|
|
176
|
+
* A password hash is the reason this exists.
|
|
177
|
+
*/
|
|
178
|
+
readonly hidden?: boolean;
|
|
179
|
+
/** Show the field, refuse to write it. Generated columns are already this. */
|
|
180
|
+
readonly readOnly?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Write the field, never read it back. The mirror of `readOnly`.
|
|
183
|
+
*
|
|
184
|
+
* **Enforced, not cosmetic.** The column is left out of the query the adapter
|
|
185
|
+
* makes and out of the projection applied to the result, so it is absent from
|
|
186
|
+
* a list, from a detail page and from the record a write returns - while
|
|
187
|
+
* still being accepted in the write itself.
|
|
188
|
+
*
|
|
189
|
+
* A password is what this is for. `hidden` is the wrong tool: it refuses the
|
|
190
|
+
* field in both directions, so a hidden password column can never be set.
|
|
191
|
+
*/
|
|
192
|
+
readonly writeOnly?: boolean;
|
|
193
|
+
/** What to call it, when the column name is not what people call the thing. */
|
|
194
|
+
readonly label?: string;
|
|
195
|
+
/** How to edit it. See {@link FieldWidget}. */
|
|
196
|
+
readonly widget?: FieldWidget;
|
|
197
|
+
/** Where it sits among the others. Lower comes first; unset comes last. */
|
|
198
|
+
readonly order?: number;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Icons a model may be given in the navigation.
|
|
202
|
+
*
|
|
203
|
+
* A closed list, for the same reason `FieldWidget` is one: the interface has to
|
|
204
|
+
* know how to draw each name, so an open string would mean silently rendering
|
|
205
|
+
* nothing and no way to notice. It is also a bundle decision - the icon set has
|
|
206
|
+
* about fifteen hundred entries, and only the ones named here are shipped.
|
|
207
|
+
*
|
|
208
|
+
* Chosen to cover what an admin's resources usually are rather than to be
|
|
209
|
+
* complete. A model with no icon is drawn without one, which is the default and
|
|
210
|
+
* is not a lesser state: identical icons down a column are decoration, and the
|
|
211
|
+
* navigation reads better with none than with thirty of the same shape.
|
|
212
|
+
*/
|
|
213
|
+
type ModelIcon = 'users' | 'user' | 'building' | 'box' | 'package' | 'tag' | 'shopping-cart' | 'credit-card' | 'receipt' | 'file-text' | 'folder' | 'image' | 'calendar' | 'clock' | 'mail' | 'message-square' | 'bell' | 'star' | 'map-pin' | 'globe' | 'settings' | 'key' | 'shield' | 'database' | 'table' | 'layers' | 'list' | 'chart-bar' | 'activity' | 'truck' | 'gift' | 'bookmark' | 'link';
|
|
214
|
+
interface ModelOverride {
|
|
215
|
+
/** What to call the model. */
|
|
216
|
+
readonly label?: string;
|
|
217
|
+
/**
|
|
218
|
+
* Which icon to show beside it in the navigation.
|
|
219
|
+
*
|
|
220
|
+
* Presentational: the client may ignore it, and nothing depends on it being
|
|
221
|
+
* honoured. See {@link ModelIcon} for why the list is closed.
|
|
222
|
+
*/
|
|
223
|
+
readonly icon?: ModelIcon;
|
|
224
|
+
/**
|
|
225
|
+
* Which field names a record, overriding what would be detected.
|
|
226
|
+
*
|
|
227
|
+
* The detection rule guesses well on conventional schemas and has no way to
|
|
228
|
+
* know that a `code` column is the one people recognise.
|
|
229
|
+
*/
|
|
230
|
+
readonly displayField?: string;
|
|
231
|
+
/** Where the model sits in the resource list. Lower first; unset last. */
|
|
232
|
+
readonly order?: number;
|
|
233
|
+
readonly fields?: Readonly<Record<string, FieldOverride>>;
|
|
234
|
+
}
|
|
235
|
+
type ModelOverrides = Readonly<Record<string, ModelOverride>>;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Where admin accounts live, as a contract.
|
|
239
|
+
*
|
|
240
|
+
* ## Why this exists at all
|
|
241
|
+
*
|
|
242
|
+
* Until 0.9.0 the answer to "who may open the admin?" was always the host
|
|
243
|
+
* application's: it already had sessions, and `AdminAuth` asked it one
|
|
244
|
+
* question. That is still right for a team that has an identity system, and
|
|
245
|
+
* nothing about it changes.
|
|
246
|
+
*
|
|
247
|
+
* It is a wall for everyone else. An application with no login of its own had
|
|
248
|
+
* to write a password hash, a session cookie and a form before the admin could
|
|
249
|
+
* go anywhere near production - which is a strange thing to ask of a package
|
|
250
|
+
* whose whole claim is that you do not build an admin.
|
|
251
|
+
*
|
|
252
|
+
* ## Why it is a contract rather than a table
|
|
253
|
+
*
|
|
254
|
+
* The same reason `OrmAdapter` is. An admin whose accounts can only live in
|
|
255
|
+
* Prisma has learned about Prisma, and the second ORM would find out the hard
|
|
256
|
+
* way. Everything here is plain data and promises; nothing knows what a
|
|
257
|
+
* database is.
|
|
258
|
+
*
|
|
259
|
+
* ## These accounts are not the application's users
|
|
260
|
+
*
|
|
261
|
+
* Deliberately, and this is the point most worth getting right. The people who
|
|
262
|
+
* administer a system are usually not rows in the table they administer, and
|
|
263
|
+
* conflating the two means a customer record with a password that opens the
|
|
264
|
+
* admin. The store is separate storage - a different model, or a different
|
|
265
|
+
* database entirely - and the admin never reads or writes the application's
|
|
266
|
+
* own users to decide who may sign in.
|
|
267
|
+
*/
|
|
268
|
+
/** One account that may sign in to the admin. */
|
|
269
|
+
interface AdminAccount {
|
|
270
|
+
readonly id: string;
|
|
271
|
+
/**
|
|
272
|
+
* What is typed into the login form.
|
|
273
|
+
*
|
|
274
|
+
* Called `email` because that is what it almost always is, and a name people
|
|
275
|
+
* recognise is worth more than one that covers a case nobody has. A store is
|
|
276
|
+
* free to hold usernames in it.
|
|
277
|
+
*/
|
|
278
|
+
readonly email: string;
|
|
279
|
+
/** Shown in the interface. Falls back to the email when absent. */
|
|
280
|
+
readonly name?: string | undefined;
|
|
281
|
+
/**
|
|
282
|
+
* The stored password hash, in whatever form the hasher produced.
|
|
283
|
+
*
|
|
284
|
+
* Read by the sign-in check and by nothing else. It must never reach a
|
|
285
|
+
* response, and the account the interface is told about is a projection that
|
|
286
|
+
* does not include it.
|
|
287
|
+
*/
|
|
288
|
+
readonly passwordHash: string;
|
|
289
|
+
/**
|
|
290
|
+
* Suspended without being deleted.
|
|
291
|
+
*
|
|
292
|
+
* Distinct from removing the row: an account that has done things is worth
|
|
293
|
+
* keeping for the record, and "cannot sign in" is not the same fact as
|
|
294
|
+
* "never existed".
|
|
295
|
+
*/
|
|
296
|
+
readonly disabled?: boolean | undefined;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The account as the interface may see it.
|
|
300
|
+
*
|
|
301
|
+
* A separate type rather than a comment on {@link AdminAccount}, because "do
|
|
302
|
+
* not send the hash" is a rule that gets forgotten and a type that cannot
|
|
303
|
+
* carry it does not.
|
|
304
|
+
*/
|
|
305
|
+
interface AdminAccountSummary {
|
|
306
|
+
readonly id: string;
|
|
307
|
+
readonly email: string;
|
|
308
|
+
readonly name?: string | undefined;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* How the admin reaches its accounts.
|
|
312
|
+
*
|
|
313
|
+
* Read-only by design. Creating and editing accounts is the application's
|
|
314
|
+
* business: it owns the storage, it knows whether that is a migration, a seed
|
|
315
|
+
* script or a form somewhere else, and an admin that could mint its own
|
|
316
|
+
* administrators is an escalation waiting for its first mistake.
|
|
317
|
+
*/
|
|
318
|
+
interface AdminAccountStore {
|
|
319
|
+
/**
|
|
320
|
+
* Find an account by what was typed into the login form.
|
|
321
|
+
*
|
|
322
|
+
* Matching is the store's decision, and it should be case-insensitive on the
|
|
323
|
+
* local part in practice: someone who registered as `Ada@example.com` will
|
|
324
|
+
* type `ada@example.com` eventually.
|
|
325
|
+
*
|
|
326
|
+
* Returns `null` when there is none. The caller must not behave observably
|
|
327
|
+
* differently for `null` than for a wrong password - see the sign-in code.
|
|
328
|
+
*/
|
|
329
|
+
findByEmail(email: string): Promise<AdminAccount | null>;
|
|
330
|
+
/**
|
|
331
|
+
* Find an account by its id, for a request that arrives with a session.
|
|
332
|
+
*
|
|
333
|
+
* Called on every authenticated request, so it should be cheap. It is also
|
|
334
|
+
* what makes a disabled or deleted account stop working immediately rather
|
|
335
|
+
* than when its session happens to expire.
|
|
336
|
+
*/
|
|
337
|
+
findById(id: string): Promise<AdminAccount | null>;
|
|
338
|
+
/**
|
|
339
|
+
* How many accounts exist.
|
|
340
|
+
*
|
|
341
|
+
* Used once, at startup, to say so when the answer is zero - an admin nobody
|
|
342
|
+
* can sign in to is a configuration mistake that otherwise announces itself
|
|
343
|
+
* as a login form that rejects everything.
|
|
344
|
+
*/
|
|
345
|
+
count(): Promise<number>;
|
|
346
|
+
/**
|
|
347
|
+
* Note that an account signed in. Optional.
|
|
348
|
+
*
|
|
349
|
+
* A store that does not care about this can leave it out; the sign-in path
|
|
350
|
+
* does not wait for it and a failure is logged rather than surfaced, because
|
|
351
|
+
* "your login worked but we could not write down that it did" is not
|
|
352
|
+
* something the person signing in can act on.
|
|
353
|
+
*/
|
|
354
|
+
recordLogin?(id: string): Promise<void>;
|
|
355
|
+
/**
|
|
356
|
+
* What this store reads, for diagnostics. Optional.
|
|
357
|
+
*
|
|
358
|
+
* A model name, a table, a directory - whatever names the storage in a way a
|
|
359
|
+
* person would recognise. It exists so a startup check can say something
|
|
360
|
+
* useful rather than something generic: an admin that exposes its own
|
|
361
|
+
* account model as an editable resource is an escalation, and a warning that
|
|
362
|
+
* cannot name the model is a warning nobody acts on.
|
|
363
|
+
*
|
|
364
|
+
* Never used to decide anything, and never sent to a client.
|
|
365
|
+
*/
|
|
366
|
+
readonly describes?: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* ORM-independent query description.
|
|
371
|
+
*
|
|
372
|
+
* The admin UI and the HTTP layer speak only this vocabulary; each adapter is
|
|
373
|
+
* responsible for translating it into its own query language.
|
|
374
|
+
*
|
|
375
|
+
* @experimental Draft contract. Expected to change during MVP implementation.
|
|
376
|
+
*/
|
|
377
|
+
type SortDirection = 'asc' | 'desc';
|
|
378
|
+
interface SortRule {
|
|
379
|
+
readonly field: string;
|
|
380
|
+
readonly direction: SortDirection;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The deliberately small operator set the MVP targets. Anything richer
|
|
384
|
+
* (nested relation filters, OR/AND trees, full-text) is a later concern and
|
|
385
|
+
* should extend this union rather than bypass it.
|
|
386
|
+
*/
|
|
387
|
+
type FilterOperator = 'eq' | 'ne' | 'contains' | 'startsWith' | 'endsWith' | 'gt' | 'gte' | 'lt' | 'lte' | 'in';
|
|
388
|
+
interface FilterRule {
|
|
389
|
+
readonly field: string;
|
|
390
|
+
readonly operator: FilterOperator;
|
|
391
|
+
readonly value: unknown;
|
|
392
|
+
}
|
|
393
|
+
/** Page-number based pagination. Cursor pagination is a later addition. */
|
|
394
|
+
interface ListQuery {
|
|
395
|
+
readonly page?: number;
|
|
396
|
+
readonly perPage?: number;
|
|
397
|
+
readonly sort?: readonly SortRule[];
|
|
398
|
+
readonly filters?: readonly FilterRule[];
|
|
399
|
+
/** Free-text term the adapter applies across searchable string fields. */
|
|
400
|
+
readonly search?: string;
|
|
401
|
+
/**
|
|
402
|
+
* The fields this query may touch, and the only ones it should return.
|
|
403
|
+
*
|
|
404
|
+
* Set by the caller that knows which fields the admin exposes - the adapter
|
|
405
|
+
* reads a schema, not a configuration. Without it, a field the application
|
|
406
|
+
* hid would still be searched by free text, sortable, filterable and
|
|
407
|
+
* returned, because from the adapter's side it is an ordinary column.
|
|
408
|
+
*
|
|
409
|
+
* Omitted means "every field the model has".
|
|
410
|
+
*/
|
|
411
|
+
readonly fields?: readonly string[];
|
|
412
|
+
}
|
|
413
|
+
interface Page<T> {
|
|
414
|
+
readonly data: readonly T[];
|
|
415
|
+
readonly total: number;
|
|
416
|
+
readonly page: number;
|
|
417
|
+
readonly perPage: number;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The single seam between Nest Admin and any ORM.
|
|
422
|
+
*
|
|
423
|
+
* Adding support for a new ORM means writing one implementation of
|
|
424
|
+
* {@link OrmAdapter} and nothing else. Core, the NestJS integration, the HTTP
|
|
425
|
+
* contract and the admin UI stay untouched.
|
|
426
|
+
*
|
|
427
|
+
* @experimental Draft contract. Expected to change during MVP implementation.
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Primary key value of a single record. Composite keys are represented by
|
|
432
|
+
* {@link ModelMetadata.primaryKey}; supporting them at this level is a
|
|
433
|
+
* post-MVP change.
|
|
434
|
+
*/
|
|
435
|
+
type RecordId = string | number;
|
|
436
|
+
/** An untyped record as it crosses the adapter boundary. */
|
|
437
|
+
type RecordData = Record<string, unknown>;
|
|
438
|
+
interface OrmAdapter {
|
|
439
|
+
/** Stable identifier used in diagnostics, e.g. `'prisma'`. */
|
|
440
|
+
readonly name: string;
|
|
441
|
+
/**
|
|
442
|
+
* Discover the models the adapter can serve. Asynchronous because an adapter
|
|
443
|
+
* may need to read a schema file or import a generated client.
|
|
444
|
+
*/
|
|
445
|
+
getModels(): Promise<readonly ModelMetadata[]>;
|
|
446
|
+
list(model: string, query: ListQuery): Promise<Page<RecordData>>;
|
|
447
|
+
findOne(model: string, id: RecordId): Promise<RecordData | null>;
|
|
448
|
+
create(model: string, data: RecordData): Promise<RecordData>;
|
|
449
|
+
update(model: string, id: RecordId, data: RecordData): Promise<RecordData>;
|
|
450
|
+
delete(model: string, id: RecordId): Promise<void>;
|
|
451
|
+
/**
|
|
452
|
+
* A page of the records on the far side of a to-many relation.
|
|
453
|
+
*
|
|
454
|
+
* Paginated for the same reason a list is: the number of children is a
|
|
455
|
+
* property of the data, not of the schema, and a parent with fifty thousand
|
|
456
|
+
* of them must not be a page that never loads.
|
|
457
|
+
*
|
|
458
|
+
* Kept separate from `list` rather than expressed as a filter because a
|
|
459
|
+
* many-to-many has no column to filter on - the link lives in a join table.
|
|
460
|
+
* A one-to-many could be asked for either way; going through one method means
|
|
461
|
+
* the caller does not have to know which it is looking at.
|
|
462
|
+
*/
|
|
463
|
+
listRelated(model: string, id: RecordId, relationField: string, query: ListQuery): Promise<Page<RecordData>>;
|
|
464
|
+
/**
|
|
465
|
+
* Link an existing record to this one.
|
|
466
|
+
*
|
|
467
|
+
* Across a many-to-many this adds a row to the join table and changes
|
|
468
|
+
* neither record. Across a one-to-many it rewrites the child's foreign key,
|
|
469
|
+
* which also **removes it from whatever parent held it** - the same operation
|
|
470
|
+
* with a consequence the caller should have been told about. Deciding whether
|
|
471
|
+
* to warn is the transport layer's job; the adapter performs what it is asked.
|
|
472
|
+
*/
|
|
473
|
+
attachRelated(model: string, id: RecordId, relationField: string, targetId: RecordId): Promise<void>;
|
|
474
|
+
/**
|
|
475
|
+
* Unlink a record from this one, without deleting either.
|
|
476
|
+
*
|
|
477
|
+
* Across a one-to-many this clears the child's foreign key, which is
|
|
478
|
+
* impossible when that column is required - see `detachBlockedReason`. The
|
|
479
|
+
* adapter may assume the caller has checked, and will surface the database's
|
|
480
|
+
* own refusal if it has not.
|
|
481
|
+
*/
|
|
482
|
+
detachRelated(model: string, id: RecordId, relationField: string, targetId: RecordId): Promise<void>;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Which models the admin exposes at all.
|
|
487
|
+
*
|
|
488
|
+
* Distinct from resource authorization, and the two answer different questions.
|
|
489
|
+
* A `ResourceSelection` is structural: it decides what the admin *is*, the same
|
|
490
|
+
* for everyone, and a model outside it does not exist as far as the admin is
|
|
491
|
+
* concerned. `AdminResourceAuth` is per-principal: the model exists, and this
|
|
492
|
+
* caller may or may not act on it.
|
|
493
|
+
*
|
|
494
|
+
* That difference is visible in the response. An excluded model answers 404 -
|
|
495
|
+
* there is no such resource - where a denied one answers 403.
|
|
496
|
+
*/
|
|
497
|
+
interface ResourceSelection {
|
|
498
|
+
/**
|
|
499
|
+
* When present, only these models are exposed. Everything else is dropped,
|
|
500
|
+
* including models added to the schema later - which is the point: an
|
|
501
|
+
* allow-list does not quietly grow when someone edits the schema.
|
|
502
|
+
*/
|
|
503
|
+
readonly include?: readonly string[];
|
|
504
|
+
/**
|
|
505
|
+
* Models removed from the selection, applied after `include`.
|
|
506
|
+
*
|
|
507
|
+
* The usual reason is a table that is not domain data: session stores,
|
|
508
|
+
* migration bookkeeping, queue tables.
|
|
509
|
+
*/
|
|
510
|
+
readonly exclude?: readonly string[];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Framework error vocabulary.
|
|
515
|
+
*
|
|
516
|
+
* Deliberately small. These exist so that adapters raise ORM-independent
|
|
517
|
+
* errors and the transport layer can map them to status codes without knowing
|
|
518
|
+
* which ORM produced them. Resist growing this taxonomy - add a new type only
|
|
519
|
+
* when a caller genuinely needs to branch on it.
|
|
520
|
+
*
|
|
521
|
+
* ## Why these are not identified with `instanceof`
|
|
522
|
+
*
|
|
523
|
+
* A published bundle can contain more than one copy of this module. The
|
|
524
|
+
* package ships two CommonJS entrypoints and each inlines its own copy of
|
|
525
|
+
* Core, so an error thrown inside the Prisma adapter is an instance of a
|
|
526
|
+
* *different* `FieldNotFoundError` class than the one the exception filter
|
|
527
|
+
* holds. `instanceof` compares class identity, so it answered `false` and
|
|
528
|
+
* every adapter-raised error was mapped to a generic 500 - a caller who
|
|
529
|
+
* mistyped a sort field got "internal error" instead of "unknown field".
|
|
530
|
+
*
|
|
531
|
+
* That was invisible to this repository's own tests, which resolve Core to a
|
|
532
|
+
* single source module, and only appeared when the built package was installed
|
|
533
|
+
* and run. So errors are identified by *value* rather than identity: a
|
|
534
|
+
* `Symbol.for` brand, which duplicate copies agree on by definition, plus a
|
|
535
|
+
* stable `kind` string. Neither depends on which copy created the object.
|
|
536
|
+
*
|
|
537
|
+
* `scripts/verify-packed-consumer.mjs` asserts the arrangement every release:
|
|
538
|
+
* one shared copy in ESM, one per entrypoint in CJS. If that ever changes, the
|
|
539
|
+
* count changes there first.
|
|
540
|
+
*
|
|
541
|
+
* @experimental Draft contract. Expected to change during MVP implementation.
|
|
542
|
+
*/
|
|
543
|
+
/**
|
|
544
|
+
* Stable discriminator for each error type.
|
|
545
|
+
*
|
|
546
|
+
* A declared string rather than the class, so it survives duplicate bundles,
|
|
547
|
+
* and rather than `name`, so it survives minification.
|
|
548
|
+
*/
|
|
549
|
+
type AdminErrorKind = 'unauthorized' | 'forbidden' | 'model-not-found' | 'field-not-found' | 'record-not-found' | 'invalid-query'
|
|
550
|
+
/** Application code refused the input. Its message reaches the client. */
|
|
551
|
+
| 'validation'
|
|
552
|
+
/** The database refused the write: unique, foreign key, or required. */
|
|
553
|
+
| 'constraint' | 'adapter'
|
|
554
|
+
/** A subclass that declared no kind of its own. Treated as internal. */
|
|
555
|
+
| 'unknown';
|
|
556
|
+
/**
|
|
557
|
+
* Base error type. Every error raised by Nest Admin extends it so that the
|
|
558
|
+
* NestJS integration can distinguish framework errors from application errors
|
|
559
|
+
* without depending on concrete subclasses.
|
|
560
|
+
*/
|
|
561
|
+
declare class NestAdminError extends Error {
|
|
562
|
+
/**
|
|
563
|
+
* Which error this is.
|
|
564
|
+
*
|
|
565
|
+
* Subclasses override it with a literal. The base value covers anything that
|
|
566
|
+
* extends this class without declaring one - the Prisma schema errors, for
|
|
567
|
+
* instance - which the transport layer treats as internal.
|
|
568
|
+
*/
|
|
569
|
+
readonly kind: AdminErrorKind;
|
|
570
|
+
constructor(message: string, options?: {
|
|
571
|
+
cause?: unknown;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Is this one of ours?
|
|
576
|
+
*
|
|
577
|
+
* Works across duplicate copies of this module, which `instanceof` does not.
|
|
578
|
+
*/
|
|
579
|
+
declare function isNestAdminError(value: unknown): value is NestAdminError;
|
|
580
|
+
/** The requested model is not part of the admin's resource set. */
|
|
581
|
+
declare class ModelNotFoundError extends NestAdminError {
|
|
582
|
+
readonly model: string;
|
|
583
|
+
readonly availableModels: readonly string[];
|
|
584
|
+
readonly kind: "model-not-found";
|
|
585
|
+
constructor(model: string, availableModels?: readonly string[]);
|
|
586
|
+
}
|
|
587
|
+
/** A referenced field does not exist on the model, or cannot be used this way. */
|
|
588
|
+
declare class FieldNotFoundError extends NestAdminError {
|
|
589
|
+
readonly model: string;
|
|
590
|
+
readonly field: string;
|
|
591
|
+
readonly kind: "field-not-found";
|
|
592
|
+
constructor(model: string, field: string, reason?: string);
|
|
593
|
+
}
|
|
594
|
+
/** No record matched the given identifier. */
|
|
595
|
+
declare class RecordNotFoundError extends NestAdminError {
|
|
596
|
+
readonly model: string;
|
|
597
|
+
readonly id: unknown;
|
|
598
|
+
readonly kind: "record-not-found";
|
|
599
|
+
constructor(model: string, id: unknown);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The query is structurally invalid - an unusable operator/field combination,
|
|
603
|
+
* a malformed value, or a request the adapter cannot express.
|
|
604
|
+
*/
|
|
605
|
+
declare class InvalidQueryError extends NestAdminError {
|
|
606
|
+
readonly kind: "invalid-query";
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The input is not acceptable, and the caller should be told why.
|
|
610
|
+
*
|
|
611
|
+
* Raised by application code - a hook rejecting a value, a rule the schema
|
|
612
|
+
* cannot express - rather than by the framework. It exists because such a
|
|
613
|
+
* refusal has to reach the person who typed the value, and the alternatives are
|
|
614
|
+
* wrong in one direction or the other: `InvalidQueryError` claims the *query*
|
|
615
|
+
* was malformed, and anything unrecognised becomes a generic 500 with the
|
|
616
|
+
* message withheld.
|
|
617
|
+
*
|
|
618
|
+
* The message **is** forwarded to the client, which is the point of it and also
|
|
619
|
+
* the responsibility that comes with it: whatever goes in is published.
|
|
620
|
+
*
|
|
621
|
+
* Naming the fields it is about is optional and worth doing. An interface that
|
|
622
|
+
* knows which input was refused can say so next to that input, where the person
|
|
623
|
+
* is looking, instead of in a banner above a form they then have to re-read.
|
|
624
|
+
*/
|
|
625
|
+
declare class ValidationError extends NestAdminError {
|
|
626
|
+
/** The inputs this is about. Empty when it is about the record as a whole. */
|
|
627
|
+
readonly fields: readonly string[];
|
|
628
|
+
readonly kind: "validation";
|
|
629
|
+
constructor(message: string,
|
|
630
|
+
/** The inputs this is about. Empty when it is about the record as a whole. */
|
|
631
|
+
fields?: readonly string[], options?: {
|
|
632
|
+
cause?: unknown;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* What the database refused, and about which fields.
|
|
637
|
+
*
|
|
638
|
+
* The distinction that matters is between a request that is *wrong* and a
|
|
639
|
+
* database that is *broken*. A duplicate email, a foreign key pointing at
|
|
640
|
+
* nothing, a missing required value - these are ordinary mistakes a person
|
|
641
|
+
* makes in a form, and until they were told apart from a real failure the admin
|
|
642
|
+
* answered every one of them with "an internal error occurred".
|
|
643
|
+
*
|
|
644
|
+
* The message is built here, from the constraint and the field names, rather
|
|
645
|
+
* than taken from the ORM. An ORM's own text carries file paths, generated
|
|
646
|
+
* query fragments and the values that collided, none of which should be
|
|
647
|
+
* published - which is exactly why the generic 500 existed in the first place.
|
|
648
|
+
*/
|
|
649
|
+
type ConstraintKind =
|
|
650
|
+
/** A value that has to be unique is not. */
|
|
651
|
+
'unique'
|
|
652
|
+
/** A reference points at a record that is not there, or is still referenced. */
|
|
653
|
+
| 'foreign-key'
|
|
654
|
+
/** A value the database requires was not supplied. */
|
|
655
|
+
| 'required';
|
|
656
|
+
declare class ConstraintError extends NestAdminError {
|
|
657
|
+
readonly constraint: ConstraintKind;
|
|
658
|
+
readonly model: string;
|
|
659
|
+
/** The columns involved. Empty when the ORM did not say. */
|
|
660
|
+
readonly fields: readonly string[];
|
|
661
|
+
readonly kind: "constraint";
|
|
662
|
+
constructor(constraint: ConstraintKind, model: string,
|
|
663
|
+
/** The columns involved. Empty when the ORM did not say. */
|
|
664
|
+
fields?: readonly string[]);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* The underlying ORM or database failed. Always wraps the original error as
|
|
668
|
+
* `cause` so the real failure is never lost.
|
|
669
|
+
*/
|
|
670
|
+
declare class AdapterError extends NestAdminError {
|
|
671
|
+
readonly kind: "adapter";
|
|
672
|
+
constructor(message: string, options?: {
|
|
673
|
+
cause?: unknown;
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* No authenticated identity was presented with the request.
|
|
678
|
+
*
|
|
679
|
+
* Raised by the host application's admin auth implementation, never by Core
|
|
680
|
+
* itself - Core has no notion of a request, a header or a session, and must
|
|
681
|
+
* not acquire one. It exists here so the transport layer can map it without
|
|
682
|
+
* knowing which framework produced it.
|
|
683
|
+
*
|
|
684
|
+
* The default message is deliberately uninformative. An authentication failure
|
|
685
|
+
* must not reveal whether a credential was absent, malformed, expired or
|
|
686
|
+
* simply wrong.
|
|
687
|
+
*/
|
|
688
|
+
declare class UnauthorizedError extends NestAdminError {
|
|
689
|
+
readonly kind: "unauthorized";
|
|
690
|
+
constructor(message?: string);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* An identity was established, but it is not permitted to do this.
|
|
694
|
+
*
|
|
695
|
+
* Deliberately distinct from {@link UnauthorizedError}: collapsing the two
|
|
696
|
+
* leaves a client unable to tell "log in" from "you cannot do this", and
|
|
697
|
+
* pushes that guesswork into every consumer.
|
|
698
|
+
*/
|
|
699
|
+
declare class ForbiddenError extends NestAdminError {
|
|
700
|
+
readonly kind: "forbidden";
|
|
701
|
+
constructor(message?: string);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Buttons the application adds.
|
|
706
|
+
*
|
|
707
|
+
* CRUD covers what a schema implies. It does not cover "publish", "resend the
|
|
708
|
+
* invitation", "recalculate the total" - operations that are obvious to the
|
|
709
|
+
* domain and invisible to the database. Without somewhere to put them, the
|
|
710
|
+
* usual answer is a second internal tool beside the admin.
|
|
711
|
+
*
|
|
712
|
+
* An action is defined once and drawn by the interface from metadata, so adding
|
|
713
|
+
* one is a server-side change and needs no rebuild of the UI.
|
|
714
|
+
*
|
|
715
|
+
* ## Authorization
|
|
716
|
+
*
|
|
717
|
+
* Actions are a distinct operation, `'action'`, rather than folded into
|
|
718
|
+
* `update`. An action can do anything, including things no CRUD route offers,
|
|
719
|
+
* so a policy should be able to decide about it separately - and a policy
|
|
720
|
+
* written before actions existed denies the unfamiliar value, which is the
|
|
721
|
+
* right direction to fail in.
|
|
722
|
+
*
|
|
723
|
+
* Actions the principal may not run are absent from the metadata, so the
|
|
724
|
+
* interface does not draw a button that would be refused.
|
|
725
|
+
*/
|
|
726
|
+
|
|
727
|
+
/** What an action reports back. */
|
|
728
|
+
interface AdminActionResult {
|
|
729
|
+
/**
|
|
730
|
+
* A sentence for the person who pressed the button.
|
|
731
|
+
*
|
|
732
|
+
* Shown as-is, so it is published - the same responsibility as a
|
|
733
|
+
* `ValidationError` message.
|
|
734
|
+
*/
|
|
735
|
+
readonly message?: string;
|
|
736
|
+
}
|
|
737
|
+
interface AdminAction {
|
|
738
|
+
/** Stable identifier, used in the URL. Letters, digits, `-` and `_`. */
|
|
739
|
+
readonly name: string;
|
|
740
|
+
/** What the button says. Defaults to `name`. */
|
|
741
|
+
readonly label?: string;
|
|
742
|
+
/**
|
|
743
|
+
* Whether the action applies to one record or to the model as a whole.
|
|
744
|
+
*
|
|
745
|
+
* A `'record'` action is offered on the detail page and receives the record's
|
|
746
|
+
* id; a `'list'` action is offered above the list and receives none.
|
|
747
|
+
*/
|
|
748
|
+
readonly scope: 'record' | 'list';
|
|
749
|
+
/**
|
|
750
|
+
* Ask before running, with this as the question.
|
|
751
|
+
*
|
|
752
|
+
* Worth setting for anything that cannot be undone. The interface refuses to
|
|
753
|
+
* proceed without an answer; it is not a substitute for the server checking
|
|
754
|
+
* that the action is permitted.
|
|
755
|
+
*/
|
|
756
|
+
readonly confirm?: string;
|
|
757
|
+
/** Draw the button as destructive. Presentation only. */
|
|
758
|
+
readonly danger?: boolean;
|
|
759
|
+
/**
|
|
760
|
+
* The work.
|
|
761
|
+
*
|
|
762
|
+
* Throw to refuse: a `ValidationError` reaches the caller with its message,
|
|
763
|
+
* anything else becomes a 500 with the message withheld.
|
|
764
|
+
*/
|
|
765
|
+
readonly run: (args: {
|
|
766
|
+
readonly context: ExecutionContext;
|
|
767
|
+
readonly model: string;
|
|
768
|
+
/** Present for a `'record'` action, absent for a `'list'` one. */
|
|
769
|
+
readonly id?: RecordId;
|
|
770
|
+
}) => AdminActionResult | void | Promise<AdminActionResult | void>;
|
|
771
|
+
}
|
|
772
|
+
/** Actions per model. Models without an entry have none. */
|
|
773
|
+
type AdminActionsByModel = Readonly<Record<string, readonly AdminAction[]>>;
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Application code that runs around a write.
|
|
777
|
+
*
|
|
778
|
+
* The admin is generic on purpose: it knows a schema, not a domain. Hashing a
|
|
779
|
+
* password, deriving a slug, writing an audit row, sending a notification -
|
|
780
|
+
* none of these can be inferred from a column type, and all of them are the
|
|
781
|
+
* reason a real application eventually stops using a generic admin. This is the
|
|
782
|
+
* seam where they go in.
|
|
783
|
+
*
|
|
784
|
+
* ## Where they run
|
|
785
|
+
*
|
|
786
|
+
* After authorization and after validation, immediately around the adapter
|
|
787
|
+
* call. A hook is therefore never reached for a request that would have been
|
|
788
|
+
* refused, and never sees a payload naming a hidden or read-only field.
|
|
789
|
+
*
|
|
790
|
+
* ## What a `before` hook returns
|
|
791
|
+
*
|
|
792
|
+
* The data to write. Returning a changed object is how a value is added or
|
|
793
|
+
* rewritten, and returning the object unchanged is fine. It is not applied
|
|
794
|
+
* blindly: the result is validated again, so a hook cannot introduce a field
|
|
795
|
+
* the admin refuses to write.
|
|
796
|
+
*
|
|
797
|
+
* ## Failing
|
|
798
|
+
*
|
|
799
|
+
* Throw. A `ValidationError` reaches the caller with its message intact and is
|
|
800
|
+
* the way to refuse an input for a reason a person should read. Anything else
|
|
801
|
+
* becomes a 500 with the message withheld, which is the right treatment for a
|
|
802
|
+
* hook that broke rather than one that objected.
|
|
803
|
+
*
|
|
804
|
+
* Nothing is transactional. An `after` hook that throws leaves the write
|
|
805
|
+
* already done, so it should be used for work whose failure is not worse than
|
|
806
|
+
* its absence - and anything that must be atomic belongs in the application's
|
|
807
|
+
* own transaction, not here.
|
|
808
|
+
*/
|
|
809
|
+
|
|
810
|
+
/** What every hook is given. */
|
|
811
|
+
interface AdminHookContext {
|
|
812
|
+
/**
|
|
813
|
+
* The NestJS execution context for the request being served.
|
|
814
|
+
*
|
|
815
|
+
* Reach the principal through it, exactly as in `AdminAuth.authorize` and
|
|
816
|
+
* `AdminResourceAuth.authorize` - one accessor works for all three.
|
|
817
|
+
*/
|
|
818
|
+
readonly context: ExecutionContext;
|
|
819
|
+
/** The model being written. */
|
|
820
|
+
readonly model: string;
|
|
821
|
+
}
|
|
822
|
+
interface AdminHooks {
|
|
823
|
+
/** Runs before a record is created. Returns the data to write. */
|
|
824
|
+
readonly beforeCreate?: (args: AdminHookContext & {
|
|
825
|
+
readonly data: RecordData;
|
|
826
|
+
}) => RecordData | Promise<RecordData>;
|
|
827
|
+
/** Runs after a record is created. Its return value is ignored. */
|
|
828
|
+
readonly afterCreate?: (args: AdminHookContext & {
|
|
829
|
+
readonly record: RecordData;
|
|
830
|
+
}) => void | Promise<void>;
|
|
831
|
+
/**
|
|
832
|
+
* Runs before a record is updated. Returns the data to write.
|
|
833
|
+
*
|
|
834
|
+
* The data is the *patch*, not the whole record: only the fields the request
|
|
835
|
+
* named are present.
|
|
836
|
+
*/
|
|
837
|
+
readonly beforeUpdate?: (args: AdminHookContext & {
|
|
838
|
+
readonly id: RecordId;
|
|
839
|
+
readonly data: RecordData;
|
|
840
|
+
}) => RecordData | Promise<RecordData>;
|
|
841
|
+
readonly afterUpdate?: (args: AdminHookContext & {
|
|
842
|
+
readonly id: RecordId;
|
|
843
|
+
readonly record: RecordData;
|
|
844
|
+
}) => void | Promise<void>;
|
|
845
|
+
/**
|
|
846
|
+
* Runs before a record is deleted.
|
|
847
|
+
*
|
|
848
|
+
* Throw to refuse the deletion - a `ValidationError` says why in a way the
|
|
849
|
+
* caller can read.
|
|
850
|
+
*/
|
|
851
|
+
readonly beforeDelete?: (args: AdminHookContext & {
|
|
852
|
+
readonly id: RecordId;
|
|
853
|
+
}) => void | Promise<void>;
|
|
854
|
+
/** Runs after a record is deleted. The record is already gone. */
|
|
855
|
+
readonly afterDelete?: (args: AdminHookContext & {
|
|
856
|
+
readonly id: RecordId;
|
|
857
|
+
}) => void | Promise<void>;
|
|
858
|
+
}
|
|
859
|
+
/** Hooks per model. Models without an entry have none. */
|
|
860
|
+
type AdminHooksByModel = Readonly<Record<string, AdminHooks>>;
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Implemented by the consuming application and passed to
|
|
864
|
+
* `AdminModule.forRoot({ auth })`.
|
|
865
|
+
*
|
|
866
|
+
* ```ts
|
|
867
|
+
* const auth: AdminAuth = {
|
|
868
|
+
* authorize(context) {
|
|
869
|
+
* const request = context.switchToHttp().getRequest()
|
|
870
|
+
* if (!request.user) throw new UnauthorizedError()
|
|
871
|
+
* if (!request.user.isStaff) throw new ForbiddenError()
|
|
872
|
+
* },
|
|
873
|
+
* }
|
|
874
|
+
* ```
|
|
875
|
+
*/
|
|
876
|
+
interface AdminAuth {
|
|
877
|
+
/**
|
|
878
|
+
* Decide whether the request may proceed.
|
|
879
|
+
*
|
|
880
|
+
* Return (or resolve) normally to allow it. To deny it, throw:
|
|
881
|
+
*
|
|
882
|
+
* - {@link UnauthorizedError} - no identity was established. 401.
|
|
883
|
+
* - {@link ForbiddenError} - an identity exists but may not do this. 403.
|
|
884
|
+
*
|
|
885
|
+
* Throwing is the intended way to deny, because it forces the caller to say
|
|
886
|
+
* *which* denial it is. A client cannot act on "denied"; it can act on "log
|
|
887
|
+
* in" versus "you may not do this".
|
|
888
|
+
*
|
|
889
|
+
* Returning `false` is also treated as a denial, mapped to `403`, so a guard
|
|
890
|
+
* written in the reflexive NestJS style still fails closed rather than
|
|
891
|
+
* silently allowing the request. Prefer throwing: `false` cannot express the
|
|
892
|
+
* 401/403 distinction, and 403 is only the safer of the two guesses.
|
|
893
|
+
*
|
|
894
|
+
* May be synchronous or asynchronous.
|
|
895
|
+
*
|
|
896
|
+
* @param context The NestJS execution context. Use it to reach the request -
|
|
897
|
+
* including any principal the host application already attached, and the
|
|
898
|
+
* `model` route parameter on per-model routes.
|
|
899
|
+
*/
|
|
900
|
+
authorize(context: ExecutionContext): void | boolean | Promise<void | boolean>;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* An {@link AdminAuth} that permits every request. **The admin API becomes
|
|
904
|
+
* completely public.**
|
|
905
|
+
*
|
|
906
|
+
* It exists because `auth` is required, and a required option with no escape
|
|
907
|
+
* hatch pushes people toward writing their own always-allow implementation -
|
|
908
|
+
* which is the same hole, only invisible in review. This one is deliberately
|
|
909
|
+
* hard to mistake for anything else: the name says `unsafe`, and it logs a
|
|
910
|
+
* warning every time an application starts with it.
|
|
911
|
+
*
|
|
912
|
+
* Intended for local development, examples and tests. Never for a deployed
|
|
913
|
+
* application.
|
|
914
|
+
*/
|
|
915
|
+
declare function unsafeAllowAllRequests(): AdminAuth;
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* The resource authorization boundary.
|
|
919
|
+
*
|
|
920
|
+
* Phase 4 gave the host a say over whether a *request* may enter the admin at
|
|
921
|
+
* all (`AdminAuth`). This answers a narrower question: may this principal touch
|
|
922
|
+
* *this model*, for *this operation*?
|
|
923
|
+
*
|
|
924
|
+
* The two are deliberately separate contracts. A host that only needs "staff
|
|
925
|
+
* only" implements `AdminAuth` and stops; a host that needs "support can read
|
|
926
|
+
* Users but nobody outside finance sees Payment" adds this one. Folding them
|
|
927
|
+
* together would force every consumer to think about resources whether or not
|
|
928
|
+
* they have per-resource rules.
|
|
929
|
+
*
|
|
930
|
+
* Why this exists at all: a host can already deny per model from `AdminAuth`,
|
|
931
|
+
* because the guard sees `params.model`. But `GET /admin/meta` has no `:model`
|
|
932
|
+
* segment, so route-level checks cannot stop the metadata endpoint from
|
|
933
|
+
* describing every table in the database - and the admin UI renders itself from
|
|
934
|
+
* that endpoint. Resource authorization has to live where metadata is produced.
|
|
935
|
+
*/
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* What the caller is trying to do.
|
|
939
|
+
*
|
|
940
|
+
* `'metadata'` is the odd one out: it is not an operation on records, it asks
|
|
941
|
+
* whether the model should be *visible* to this principal at all. A model that
|
|
942
|
+
* fails a `'metadata'` check disappears from `GET /admin/meta` entirely.
|
|
943
|
+
*/
|
|
944
|
+
type AdminOperation = 'metadata' | 'list' | 'read' | 'create' | 'update' | 'delete'
|
|
945
|
+
/**
|
|
946
|
+
* An application-defined action.
|
|
947
|
+
*
|
|
948
|
+
* Distinct from `update` because an action can do anything, including things
|
|
949
|
+
* no CRUD route offers, so a policy should be able to decide about it
|
|
950
|
+
* separately. A policy written before actions existed does not recognise the
|
|
951
|
+
* value and denies it, which is the right direction to fail in.
|
|
952
|
+
*/
|
|
953
|
+
| 'action';
|
|
954
|
+
/** Everything the policy is given to decide with. */
|
|
955
|
+
interface ResourceAuthorization {
|
|
956
|
+
/**
|
|
957
|
+
* The NestJS execution context for the request being served. Use it to reach
|
|
958
|
+
* whatever principal the host application attached to the request - exactly
|
|
959
|
+
* as in `AdminAuth.authorize`, so one accessor works for both contracts.
|
|
960
|
+
*/
|
|
961
|
+
readonly context: ExecutionContext;
|
|
962
|
+
/** The model name as the schema declares it, e.g. `User`. */
|
|
963
|
+
readonly model: string;
|
|
964
|
+
readonly operation: AdminOperation;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Implemented by the consuming application and passed to
|
|
968
|
+
* `AdminModule.forRoot({ resourceAuth })`.
|
|
969
|
+
*
|
|
970
|
+
* ```ts
|
|
971
|
+
* const resourceAuth: AdminResourceAuth = {
|
|
972
|
+
* authorize({ context, model, operation }) {
|
|
973
|
+
* const { user } = context.switchToHttp().getRequest()
|
|
974
|
+
* if (model === 'AuditLog') return user.isAdmin
|
|
975
|
+
* if (operation === 'delete') return user.isAdmin
|
|
976
|
+
* return true
|
|
977
|
+
* },
|
|
978
|
+
* }
|
|
979
|
+
* ```
|
|
980
|
+
*/
|
|
981
|
+
interface AdminResourceAuth {
|
|
982
|
+
/**
|
|
983
|
+
* Decide whether this principal may perform `operation` on `model`.
|
|
984
|
+
*
|
|
985
|
+
* Return `true`, or return nothing, to allow. To deny, return `false` or
|
|
986
|
+
* throw `ForbiddenError`. Both are treated identically - unlike
|
|
987
|
+
* `AdminAuth`, there is no 401/403 ambiguity to resolve here, because a
|
|
988
|
+
* request that reached this point has already passed authentication.
|
|
989
|
+
*
|
|
990
|
+
* The consequence of a denial depends on the operation:
|
|
991
|
+
*
|
|
992
|
+
* - `'metadata'` - the model is **omitted** from `GET /admin/meta`. It is not
|
|
993
|
+
* an error; the response simply describes a smaller schema.
|
|
994
|
+
* - everything else - the request fails with `403 FORBIDDEN`, and the ORM
|
|
995
|
+
* adapter is never called.
|
|
996
|
+
*
|
|
997
|
+
* Anything else thrown is treated as a bug in the host's policy: the request
|
|
998
|
+
* fails with a generic 500 and the real error is logged. A failing policy
|
|
999
|
+
* never allows access.
|
|
1000
|
+
*
|
|
1001
|
+
* May be synchronous or asynchronous.
|
|
1002
|
+
*/
|
|
1003
|
+
authorize(resource: ResourceAuthorization): void | boolean | Promise<void | boolean>;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* What an application puts on the dashboard.
|
|
1008
|
+
*
|
|
1009
|
+
* ## A closed set of four
|
|
1010
|
+
*
|
|
1011
|
+
* `count`, `list`, `chart`, `stat`. Closed for the same reason `FieldWidget` is:
|
|
1012
|
+
* the interface has to know how to draw each one, so an open string would mean
|
|
1013
|
+
* rendering nothing and no way to notice.
|
|
1014
|
+
*
|
|
1015
|
+
* It is also the line this release does not cross. An arbitrary React component
|
|
1016
|
+
* would mean the consuming application builds and bundles one, which is exactly
|
|
1017
|
+
* the thing this package exists not to make people do - and the reason custom
|
|
1018
|
+
* pages have been out of scope since 0.6.0.
|
|
1019
|
+
*
|
|
1020
|
+
* ## Three of them are declarative on purpose
|
|
1021
|
+
*
|
|
1022
|
+
* `count`, `list` and `chart` name a model and a filter; the server does the
|
|
1023
|
+
* work. That is not just terseness. A widget that names a model can be
|
|
1024
|
+
* *authorized*: one over a resource this principal cannot see is absent from
|
|
1025
|
+
* the document, the same way a hidden model and a refused action already are.
|
|
1026
|
+
* A widget built from a closure could not be checked, only trusted.
|
|
1027
|
+
*
|
|
1028
|
+
* `stat` is the escape hatch and has no model, because the number it shows may
|
|
1029
|
+
* come from anywhere - a payment processor, a queue, three tables joined. It
|
|
1030
|
+
* runs application code, so the application's own rules apply to it.
|
|
1031
|
+
*
|
|
1032
|
+
* ## Nothing is configured by default
|
|
1033
|
+
*
|
|
1034
|
+
* An admin with no `dashboard` option still gets one, built from metadata
|
|
1035
|
+
* alone: a count per model, and recent records where the schema says when a
|
|
1036
|
+
* record was created. Declaring widgets replaces that rather than adding to it,
|
|
1037
|
+
* because a dashboard is a page someone designed, and half-designed is worse
|
|
1038
|
+
* than either.
|
|
1039
|
+
*/
|
|
1040
|
+
|
|
1041
|
+
/** How wide a widget sits in the four-column grid. */
|
|
1042
|
+
type WidgetSpan = 1 | 2 | 3 | 4;
|
|
1043
|
+
interface Common {
|
|
1044
|
+
/** Shown above it. The one thing every widget needs. */
|
|
1045
|
+
readonly title: string;
|
|
1046
|
+
/** A sentence under the title, when the title cannot carry it alone. */
|
|
1047
|
+
readonly description?: string;
|
|
1048
|
+
/** Columns out of four. Sensible per kind when omitted. */
|
|
1049
|
+
readonly span?: WidgetSpan;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* A single number, from a model.
|
|
1053
|
+
*
|
|
1054
|
+
* The most common thing on any dashboard, and the reason it is declarative:
|
|
1055
|
+
* "how many open orders" is a model, a filter, and nothing else.
|
|
1056
|
+
*/
|
|
1057
|
+
interface CountWidget extends Common {
|
|
1058
|
+
readonly kind: 'count';
|
|
1059
|
+
readonly model: string;
|
|
1060
|
+
/** `field:op:value`, the same syntax the list screen's URL uses. */
|
|
1061
|
+
readonly filter?: string;
|
|
1062
|
+
/**
|
|
1063
|
+
* Compare against the same count a period ago, and show the change.
|
|
1064
|
+
*
|
|
1065
|
+
* Needs the model to have a creation timestamp; the comparison is silently
|
|
1066
|
+
* omitted when it does not, rather than the widget disappearing.
|
|
1067
|
+
*/
|
|
1068
|
+
readonly compareDays?: number;
|
|
1069
|
+
}
|
|
1070
|
+
/** A few records, most recent first where the model says which those are. */
|
|
1071
|
+
interface ListWidget extends Common {
|
|
1072
|
+
readonly kind: 'list';
|
|
1073
|
+
readonly model: string;
|
|
1074
|
+
readonly filter?: string;
|
|
1075
|
+
/** How many rows. Five by default; more than ten belongs on the list screen. */
|
|
1076
|
+
readonly limit?: number;
|
|
1077
|
+
}
|
|
1078
|
+
/** How many records appeared per day, week or month. */
|
|
1079
|
+
interface ChartWidget extends Common {
|
|
1080
|
+
readonly kind: 'chart';
|
|
1081
|
+
readonly model: string;
|
|
1082
|
+
readonly filter?: string;
|
|
1083
|
+
readonly bucket?: 'day' | 'week' | 'month';
|
|
1084
|
+
/** How many buckets. Thirty by default, ninety at most - see the service. */
|
|
1085
|
+
readonly buckets?: number;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* A number the application works out for itself.
|
|
1089
|
+
*
|
|
1090
|
+
* The escape hatch, and the only widget that runs application code. Whatever it
|
|
1091
|
+
* returns is shown; whatever it throws becomes a widget that says it could not
|
|
1092
|
+
* load, rather than a dashboard that does not.
|
|
1093
|
+
*/
|
|
1094
|
+
interface StatWidget extends Common {
|
|
1095
|
+
readonly kind: 'stat';
|
|
1096
|
+
readonly load: (args: {
|
|
1097
|
+
readonly context: ExecutionContext;
|
|
1098
|
+
}) => Promise<StatResult> | StatResult;
|
|
1099
|
+
}
|
|
1100
|
+
interface StatResult {
|
|
1101
|
+
/** Shown large. A string is passed through, so it can carry a currency. */
|
|
1102
|
+
readonly value: string | number;
|
|
1103
|
+
/** A change against some previous period, as a percentage. */
|
|
1104
|
+
readonly delta?: number;
|
|
1105
|
+
/** Under the value. "vs last month", "across 4 regions". */
|
|
1106
|
+
readonly hint?: string;
|
|
1107
|
+
}
|
|
1108
|
+
type DashboardWidget = CountWidget | ListWidget | ChartWidget | StatWidget;
|
|
1109
|
+
/**
|
|
1110
|
+
* The dashboard an application declares.
|
|
1111
|
+
*
|
|
1112
|
+
* An array rather than a keyed object: a dashboard is read top to bottom, and
|
|
1113
|
+
* the order things appear in is part of the design.
|
|
1114
|
+
*/
|
|
1115
|
+
type AdminDashboard = readonly DashboardWidget[];
|
|
1116
|
+
|
|
1117
|
+
interface AdminTheme {
|
|
1118
|
+
/**
|
|
1119
|
+
* Accent colour, as a CSS hex value - `#0b6e6e` or `#0b6`.
|
|
1120
|
+
*
|
|
1121
|
+
* Hex only. A full CSS colour grammar would mean parsing one, and a value
|
|
1122
|
+
* this small is not worth a parser; named colours and `rgb()` are excluded
|
|
1123
|
+
* for the same reason.
|
|
1124
|
+
*/
|
|
1125
|
+
readonly brandColor?: string;
|
|
1126
|
+
/** Page title and the name shown in the header. Plain text. */
|
|
1127
|
+
readonly title?: string;
|
|
1128
|
+
/**
|
|
1129
|
+
* Logo shown beside the title.
|
|
1130
|
+
*
|
|
1131
|
+
* An `http(s)` URL or a `data:image/...` URI. Other schemes are refused:
|
|
1132
|
+
* `javascript:` in an image source is the obvious one, but the rule is a
|
|
1133
|
+
* whitelist rather than a blacklist so there is nothing to keep up with.
|
|
1134
|
+
*/
|
|
1135
|
+
readonly logoUrl?: string;
|
|
1136
|
+
/**
|
|
1137
|
+
* Which appearance to start from, before anyone chooses.
|
|
1138
|
+
*
|
|
1139
|
+
* `'system'` follows the viewer's operating system and is the default. A
|
|
1140
|
+
* viewer's own choice, once made, wins over this and is remembered by their
|
|
1141
|
+
* browser - so this sets the first impression rather than a policy.
|
|
1142
|
+
*/
|
|
1143
|
+
readonly appearance?: 'system' | 'light' | 'dark';
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* `AdminModule` - the NestJS integration.
|
|
1148
|
+
*
|
|
1149
|
+
* ```ts
|
|
1150
|
+
* AdminModule.forRoot({
|
|
1151
|
+
* adapter: new PrismaAdapter({ client: prisma }),
|
|
1152
|
+
* auth: myAdminAuth,
|
|
1153
|
+
* })
|
|
1154
|
+
* ```
|
|
1155
|
+
*
|
|
1156
|
+
* The module wires an `OrmAdapter` and an `AdminAuth` into the admin HTTP
|
|
1157
|
+
* layer and does nothing else. It does not construct a database client, does
|
|
1158
|
+
* not authenticate anyone, does not read configuration from disk, and holds no
|
|
1159
|
+
* module-level mutable state - so two instances in the same process cannot
|
|
1160
|
+
* interfere with each other.
|
|
1161
|
+
*
|
|
1162
|
+
* It is not `@Global()`: making a library's providers globally visible in
|
|
1163
|
+
* someone else's application is a decision the application should make.
|
|
1164
|
+
*/
|
|
1165
|
+
|
|
1166
|
+
interface AdminModuleOptions {
|
|
1167
|
+
/**
|
|
1168
|
+
* The ORM adapter the admin reads and writes through.
|
|
1169
|
+
*
|
|
1170
|
+
* Constructed by the consuming application, never by the framework: under
|
|
1171
|
+
* Prisma 7 a client is built from a driver adapter, so only the application
|
|
1172
|
+
* knows the provider, the credentials and the connection strategy.
|
|
1173
|
+
*/
|
|
1174
|
+
readonly adapter: OrmAdapter;
|
|
1175
|
+
/**
|
|
1176
|
+
* Decides whether a request may reach the admin.
|
|
1177
|
+
*
|
|
1178
|
+
* **Required, deliberately.** The admin exposes every record in the database
|
|
1179
|
+
* and, through `/admin/meta`, the shape of the entire schema. An optional
|
|
1180
|
+
* option defaulting to "open" would mean a forgotten line in a config file
|
|
1181
|
+
* silently publishes the database - the failure would be invisible until
|
|
1182
|
+
* someone else found it.
|
|
1183
|
+
*
|
|
1184
|
+
* For local development and examples, pass `unsafeAllowAllRequests()`, which
|
|
1185
|
+
* is explicit at the call site and warns at startup.
|
|
1186
|
+
*/
|
|
1187
|
+
readonly auth: AdminAuth;
|
|
1188
|
+
/**
|
|
1189
|
+
* Decides which models this principal may see and act on.
|
|
1190
|
+
*
|
|
1191
|
+
* Optional, defaulting to allowing every model. Unlike `auth`, that default
|
|
1192
|
+
* is not a hole: `auth` is required, so the door is already shut, and
|
|
1193
|
+
* omitting this only means everyone admitted sees the whole schema - exactly
|
|
1194
|
+
* the behaviour before the option existed. Requiring it would break every
|
|
1195
|
+
* existing consumer to express a rule most applications do not have.
|
|
1196
|
+
*
|
|
1197
|
+
* Supply it when some models should be invisible or read-only to some
|
|
1198
|
+
* principals. A model denied for `'metadata'` disappears from
|
|
1199
|
+
* `GET /admin/meta`; a model denied for any other operation makes the request
|
|
1200
|
+
* fail with 403 before the ORM adapter is called.
|
|
1201
|
+
*/
|
|
1202
|
+
readonly resourceAuth?: AdminResourceAuth;
|
|
1203
|
+
/**
|
|
1204
|
+
* Where the admin is mounted. Defaults to `/admin`.
|
|
1205
|
+
*
|
|
1206
|
+
* Accepts `admin`, `/admin` and `/admin/` alike, and may be nested, as in
|
|
1207
|
+
* `/internal/admin`. It cannot be empty or `/`: these routes end in
|
|
1208
|
+
* `:model`, so mounting them at the root would capture every unmatched
|
|
1209
|
+
* request in the host application.
|
|
1210
|
+
*
|
|
1211
|
+
* The API and the UI move together. There is one mount point, not two.
|
|
1212
|
+
*/
|
|
1213
|
+
readonly path?: string;
|
|
1214
|
+
/**
|
|
1215
|
+
* Which models the admin exposes at all. Defaults to every model the adapter
|
|
1216
|
+
* reports.
|
|
1217
|
+
*
|
|
1218
|
+
* Structural, and not a substitute for `resourceAuth`: this is the same for
|
|
1219
|
+
* every principal, so an excluded model answers 404 rather than 403. Use it
|
|
1220
|
+
* for tables that are not domain data - session stores, migration
|
|
1221
|
+
* bookkeeping, queues - and `resourceAuth` for who may do what.
|
|
1222
|
+
*
|
|
1223
|
+
* A name that matches no model fails at startup rather than being ignored: a
|
|
1224
|
+
* typo in `exclude` would otherwise leave the model exposed.
|
|
1225
|
+
*/
|
|
1226
|
+
readonly resources?: ResourceSelection;
|
|
1227
|
+
/**
|
|
1228
|
+
* Per-model configuration: labels, widgets, ordering, and the two that are
|
|
1229
|
+
* enforced rather than suggested - hidden and readOnly.
|
|
1230
|
+
*
|
|
1231
|
+
* A hidden field is removed from the metadata every layer reads, so it cannot
|
|
1232
|
+
* be filtered, sorted, written, or returned. A name matching no model or
|
|
1233
|
+
* field fails at startup.
|
|
1234
|
+
*/
|
|
1235
|
+
readonly models?: ModelOverrides;
|
|
1236
|
+
/**
|
|
1237
|
+
* Application code that runs around a write, per model.
|
|
1238
|
+
*
|
|
1239
|
+
* Where hashing a password, deriving a slug or writing an audit row goes -
|
|
1240
|
+
* none of which can be inferred from a column type. See `AdminHooks`.
|
|
1241
|
+
*/
|
|
1242
|
+
readonly hooks?: AdminHooksByModel;
|
|
1243
|
+
/**
|
|
1244
|
+
* Buttons the application adds, per model.
|
|
1245
|
+
*
|
|
1246
|
+
* CRUD covers what a schema implies; "publish" and "resend the invitation"
|
|
1247
|
+
* are obvious to the domain and invisible to the database. See `AdminAction`.
|
|
1248
|
+
*/
|
|
1249
|
+
readonly actions?: AdminActionsByModel;
|
|
1250
|
+
/**
|
|
1251
|
+
* Branding the served page applies without a rebuild: an accent colour, a
|
|
1252
|
+
* title, a logo. Structural, because the page is rendered before any
|
|
1253
|
+
* provider exists.
|
|
1254
|
+
*/
|
|
1255
|
+
readonly theme?: AdminTheme;
|
|
1256
|
+
/**
|
|
1257
|
+
* What the dashboard shows.
|
|
1258
|
+
*
|
|
1259
|
+
* Omit it and the dashboard is built from the schema: a count per model, the
|
|
1260
|
+
* newest records, and a month of activity. Declaring widgets replaces that
|
|
1261
|
+
* rather than adding to it - a dashboard is a page someone designed, and
|
|
1262
|
+
* half-designed is worse than either.
|
|
1263
|
+
*/
|
|
1264
|
+
readonly dashboard?: AdminDashboard;
|
|
1265
|
+
/**
|
|
1266
|
+
* Directory holding the built admin UI.
|
|
1267
|
+
*
|
|
1268
|
+
* Defaults to the copy bundled inside this package, which is what a consumer
|
|
1269
|
+
* wants and why it is optional. Overriding it exists for this repository's
|
|
1270
|
+
* own tests, which run from `src` while the built UI lives in `dist`.
|
|
1271
|
+
*
|
|
1272
|
+
* @internal
|
|
1273
|
+
*/
|
|
1274
|
+
readonly uiRoot?: string;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* What a `forRootAsync` factory returns.
|
|
1278
|
+
*
|
|
1279
|
+
* Everything except the structural options, which are decided when the module
|
|
1280
|
+
* is defined and so cannot come from a provider - see `forRootAsync`.
|
|
1281
|
+
*/
|
|
1282
|
+
type AdminModuleFactoryOptions = Omit<AdminModuleOptions, 'path' | 'uiRoot' | 'theme'>;
|
|
1283
|
+
/** Supply options from a class rather than a factory function. */
|
|
1284
|
+
interface AdminModuleOptionsFactory {
|
|
1285
|
+
createAdminOptions(): AdminModuleFactoryOptions | Promise<AdminModuleFactoryOptions>;
|
|
1286
|
+
}
|
|
1287
|
+
interface AdminModuleAsyncOptions {
|
|
1288
|
+
/** As `AdminModuleOptions.path`. Structural, so it is not from the factory. */
|
|
1289
|
+
readonly path?: string;
|
|
1290
|
+
/** @internal As `AdminModuleOptions.uiRoot`. */
|
|
1291
|
+
readonly uiRoot?: string;
|
|
1292
|
+
/**
|
|
1293
|
+
* As `AdminModuleOptions.theme`. Structural, so it is not from the factory:
|
|
1294
|
+
* the shell is rendered from it and no provider exists at that point.
|
|
1295
|
+
*/
|
|
1296
|
+
readonly theme?: AdminTheme;
|
|
1297
|
+
/** Modules whose providers the factory needs. */
|
|
1298
|
+
readonly imports?: ModuleMetadata['imports'];
|
|
1299
|
+
/** Providers passed to `useFactory`, in order. */
|
|
1300
|
+
readonly inject?: FactoryProvider['inject'];
|
|
1301
|
+
readonly useFactory?: (...args: never[]) => AdminModuleFactoryOptions | Promise<AdminModuleFactoryOptions>;
|
|
1302
|
+
/** Instantiated by Nest, then asked for the options. */
|
|
1303
|
+
readonly useClass?: Type<AdminModuleOptionsFactory>;
|
|
1304
|
+
/** An options factory the application already provides elsewhere. */
|
|
1305
|
+
readonly useExisting?: Type<AdminModuleOptionsFactory>;
|
|
1306
|
+
}
|
|
1307
|
+
declare class AdminModule {
|
|
1308
|
+
static forRoot(options: AdminModuleOptions): DynamicModule;
|
|
1309
|
+
/**
|
|
1310
|
+
* The same module, with the adapter and the auth policy resolved through DI.
|
|
1311
|
+
*
|
|
1312
|
+
* For the ordinary case where those things are not available when the module
|
|
1313
|
+
* is declared: a `PrismaService` that belongs to another module, a connection
|
|
1314
|
+
* string that comes from `ConfigService`.
|
|
1315
|
+
*
|
|
1316
|
+
* ```ts
|
|
1317
|
+
* AdminModule.forRootAsync({
|
|
1318
|
+
* imports: [PrismaModule, ConfigModule],
|
|
1319
|
+
* inject: [PrismaService, ConfigService],
|
|
1320
|
+
* useFactory: (prisma: PrismaService, config: ConfigService) => ({
|
|
1321
|
+
* adapter: new PrismaAdapter({ client: prisma }),
|
|
1322
|
+
* auth: new SessionAdminAuth(config.get('ADMIN_ROLE')),
|
|
1323
|
+
* }),
|
|
1324
|
+
* })
|
|
1325
|
+
* ```
|
|
1326
|
+
*
|
|
1327
|
+
* `path` stays on this object rather than coming from the factory. Routes are
|
|
1328
|
+
* registered when the module is defined, which is before any provider has
|
|
1329
|
+
* been instantiated, so the mount path cannot wait for an injection - and a
|
|
1330
|
+
* `path` returned from the factory would be silently ignored, which is worse
|
|
1331
|
+
* than not offering it.
|
|
1332
|
+
*/
|
|
1333
|
+
static forRootAsync(options: AdminModuleAsyncOptions): DynamicModule;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* An `AdminAuth` that ships in the box.
|
|
1338
|
+
*
|
|
1339
|
+
* ## This does not move the boundary
|
|
1340
|
+
*
|
|
1341
|
+
* `AdminAuth` is unchanged and still the only way in. An application with its
|
|
1342
|
+
* own identity system implements it and never sees any of this. What changes is
|
|
1343
|
+
* that an application *without* one no longer has to write a password hash, a
|
|
1344
|
+
* cookie and a form before the admin can be put behind a login.
|
|
1345
|
+
*
|
|
1346
|
+
* So there are three answers to "who may open this?", and a consumer picks one:
|
|
1347
|
+
*
|
|
1348
|
+
* auth: unsafeAllowAllRequests() development only, warns at startup
|
|
1349
|
+
* auth: myOwnAuth an application that already has identity
|
|
1350
|
+
* auth: builtInAuth({ ... }) a login page, sessions and a store
|
|
1351
|
+
*
|
|
1352
|
+
* ## The accounts are separate from the application's users
|
|
1353
|
+
*
|
|
1354
|
+
* By construction: the store is a contract over storage the application
|
|
1355
|
+
* nominates, and the intended shape is a model of its own. The admin never
|
|
1356
|
+
* consults the application's user table to decide who may sign in, and adding
|
|
1357
|
+
* a customer never adds someone who can administer the system.
|
|
1358
|
+
*/
|
|
1359
|
+
|
|
1360
|
+
interface BuiltInAuthOptions {
|
|
1361
|
+
/**
|
|
1362
|
+
* Where the accounts live.
|
|
1363
|
+
*
|
|
1364
|
+
* `prismaAccountStore` from `@nest-admin/nestjs/prisma` covers the usual
|
|
1365
|
+
* case; anything satisfying the contract works.
|
|
1366
|
+
*/
|
|
1367
|
+
readonly store: AdminAccountStore;
|
|
1368
|
+
readonly session: {
|
|
1369
|
+
/**
|
|
1370
|
+
* The key the session cookie is signed with. **Required.**
|
|
1371
|
+
*
|
|
1372
|
+
* At least 32 characters, checked at startup. A short secret is a
|
|
1373
|
+
* forgeable cookie, and the failure is silent: everything works, and
|
|
1374
|
+
* anybody can mint a session for any account.
|
|
1375
|
+
*
|
|
1376
|
+
* Read it from the environment. A secret in source control is a secret
|
|
1377
|
+
* everyone who has ever cloned the repository knows.
|
|
1378
|
+
*/
|
|
1379
|
+
readonly secret: string;
|
|
1380
|
+
/** How long a session lasts, in seconds. Twelve hours by default. */
|
|
1381
|
+
readonly maxAge?: number;
|
|
1382
|
+
/** The cookie's name. Change it only to avoid a collision. */
|
|
1383
|
+
readonly cookieName?: string;
|
|
1384
|
+
/**
|
|
1385
|
+
* Send the cookie only over HTTPS.
|
|
1386
|
+
*
|
|
1387
|
+
* Left unset it is decided per request: on for everything except
|
|
1388
|
+
* localhost, which is what makes the admin work in development without
|
|
1389
|
+
* being insecure anywhere else. Set it to `true` to require HTTPS always.
|
|
1390
|
+
*/
|
|
1391
|
+
readonly secure?: boolean;
|
|
1392
|
+
};
|
|
1393
|
+
/** Failed attempts before a pause. Ten by default. */
|
|
1394
|
+
readonly maxAttempts?: number;
|
|
1395
|
+
/** How long that pause lasts, in seconds. Fifteen minutes by default. */
|
|
1396
|
+
readonly lockoutSeconds?: number;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* The account this request signed in as.
|
|
1400
|
+
*
|
|
1401
|
+
* For a `resourceAuth` policy or a hook that needs to know who is asking.
|
|
1402
|
+
* `undefined` when the admin is not using the built-in auth, which is why it
|
|
1403
|
+
* is optional rather than assumed.
|
|
1404
|
+
*/
|
|
1405
|
+
declare function adminAccountOf(context: ExecutionContext): AdminAccountSummary | undefined;
|
|
1406
|
+
declare function builtInAuth(options: BuiltInAuthOptions): AdminAuth;
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Hash a password for storage.
|
|
1410
|
+
*
|
|
1411
|
+
* Exported for the application, because creating accounts is its business -
|
|
1412
|
+
* a seed script, a migration, or a form of its own. The admin never mints an
|
|
1413
|
+
* administrator; see `AdminAccountStore` for why.
|
|
1414
|
+
*/
|
|
1415
|
+
declare function hashAdminPassword(password: string): Promise<string>;
|
|
1416
|
+
/**
|
|
1417
|
+
* Does this password match this stored hash?
|
|
1418
|
+
*
|
|
1419
|
+
* Never throws for a malformed or unrecognised hash - it answers `false`. A
|
|
1420
|
+
* store holding something this function does not understand is a configuration
|
|
1421
|
+
* problem, and turning it into a 500 on the login route would tell an attacker
|
|
1422
|
+
* that the account exists and that its record is unusual.
|
|
1423
|
+
*
|
|
1424
|
+
* The comparison is `timingSafeEqual`, not `===`. String equality returns as
|
|
1425
|
+
* soon as two bytes differ, and the difference is measurable often enough to
|
|
1426
|
+
* recover a hash a byte at a time.
|
|
1427
|
+
*/
|
|
1428
|
+
declare function verifyAdminPassword(password: string, stored: string): Promise<boolean>;
|
|
1429
|
+
|
|
1430
|
+
/** A secret of the right shape, for a consumer that needs one generated. */
|
|
1431
|
+
declare function generateSessionSecret(): string;
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* The response envelope.
|
|
1435
|
+
*
|
|
1436
|
+
* Every admin endpoint returns the same two shapes, so a generic frontend can
|
|
1437
|
+
* branch on one field rather than on status codes plus per-endpoint knowledge.
|
|
1438
|
+
*
|
|
1439
|
+
* Success: { success: true, data: <payload>, meta?: <pagination> }
|
|
1440
|
+
* Failure: { success: false, error: { code, message, details? } }
|
|
1441
|
+
*
|
|
1442
|
+
* @experimental The HTTP contract is expected to change before 1.0.
|
|
1443
|
+
*/
|
|
1444
|
+
/** Pagination facts a list response carries alongside its rows. */
|
|
1445
|
+
interface PageMeta {
|
|
1446
|
+
readonly total: number;
|
|
1447
|
+
readonly page: number;
|
|
1448
|
+
readonly perPage: number;
|
|
1449
|
+
}
|
|
1450
|
+
interface SuccessResponse<T> {
|
|
1451
|
+
readonly success: true;
|
|
1452
|
+
readonly data: T;
|
|
1453
|
+
readonly meta?: PageMeta;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Stable, machine-readable error codes.
|
|
1457
|
+
*
|
|
1458
|
+
* Clients branch on these, never on the human-readable message. Adding a code
|
|
1459
|
+
* is a compatible change; renaming one is not.
|
|
1460
|
+
*/
|
|
1461
|
+
type AdminErrorCode = 'UNAUTHORIZED' | 'FORBIDDEN' | 'MODEL_NOT_FOUND' | 'RECORD_NOT_FOUND' | 'FIELD_NOT_FOUND' | 'INVALID_QUERY' | 'VALIDATION_ERROR' | 'CONSTRAINT_VIOLATION' | 'INTERNAL_ERROR';
|
|
1462
|
+
interface ErrorResponse {
|
|
1463
|
+
readonly success: false;
|
|
1464
|
+
readonly error: {
|
|
1465
|
+
readonly code: AdminErrorCode;
|
|
1466
|
+
readonly message: string;
|
|
1467
|
+
/** Structured context, e.g. `{ model, field }`. Never internal detail. */
|
|
1468
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
type AdminResponse<T> = SuccessResponse<T> | ErrorResponse;
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* The public HTTP representation of model metadata.
|
|
1475
|
+
*
|
|
1476
|
+
* This is the contract between the backend and any future admin frontend, and
|
|
1477
|
+
* it is deliberately declared separately from Core's `ModelMetadata` rather
|
|
1478
|
+
* than serialised straight from it. Two reasons:
|
|
1479
|
+
*
|
|
1480
|
+
* 1. Core's contract is marked `@experimental` and will keep moving. The wire
|
|
1481
|
+
* format must not move with it by accident.
|
|
1482
|
+
* 2. An explicit mapper is a whitelist. If a future adapter puts something
|
|
1483
|
+
* ORM-specific on `FieldMetadata`, it cannot silently reach a client.
|
|
1484
|
+
*
|
|
1485
|
+
* Nothing here mentions Prisma, DMMF, or any ORM. Replacing the Prisma adapter
|
|
1486
|
+
* with another one must not change a single byte of this shape.
|
|
1487
|
+
*
|
|
1488
|
+
* @experimental The HTTP contract is expected to change before 1.0.
|
|
1489
|
+
*/
|
|
1490
|
+
|
|
1491
|
+
/** Mirrors Core's `FieldKind`, restated so the wire format is self-contained. */
|
|
1492
|
+
type FieldKindDto = 'string' | 'number' | 'boolean' | 'datetime' | 'enum' | 'json' | 'relation' | 'unknown';
|
|
1493
|
+
interface RelationDto {
|
|
1494
|
+
readonly targetModel: string;
|
|
1495
|
+
readonly cardinality: 'one' | 'many';
|
|
1496
|
+
/**
|
|
1497
|
+
* Scalar field on this model holding the key, for a to-one relation.
|
|
1498
|
+
*
|
|
1499
|
+
* The UI needs it twice over: it is the field a form submits when the user
|
|
1500
|
+
* picks a related record, and the field a filter is expressed in. Absent on
|
|
1501
|
+
* to-many relations, which have no column on this side.
|
|
1502
|
+
*/
|
|
1503
|
+
readonly from?: string;
|
|
1504
|
+
/** Field on the target the key points at - usually its id. */
|
|
1505
|
+
readonly to?: string;
|
|
1506
|
+
/**
|
|
1507
|
+
* Where the link is stored, which decides what may be done to it.
|
|
1508
|
+
*
|
|
1509
|
+
* Computed on the server rather than left for the client to derive, for the
|
|
1510
|
+
* same reason as `displayField`: working it out needs the other half of the
|
|
1511
|
+
* relation, and two implementations of that rule would drift. A client that
|
|
1512
|
+
* guessed wrong would offer a button that cannot work.
|
|
1513
|
+
*/
|
|
1514
|
+
readonly shape?: 'to-one' | 'one-to-many' | 'many-to-many';
|
|
1515
|
+
/**
|
|
1516
|
+
* Why records cannot be detached from this relation, when they cannot.
|
|
1517
|
+
*
|
|
1518
|
+
* Present only for a one-to-many whose child key is required: such a child
|
|
1519
|
+
* cannot exist without a parent, so there is nothing to detach it to.
|
|
1520
|
+
*/
|
|
1521
|
+
readonly detachBlocked?: string;
|
|
1522
|
+
/**
|
|
1523
|
+
* The column on the target model that points back at this one.
|
|
1524
|
+
*
|
|
1525
|
+
* What "all the posts by this author" is expressed as:
|
|
1526
|
+
* `?filter=<targetForeignKey>:eq:<parentId>`. Present only for a one-to-many,
|
|
1527
|
+
* since a many-to-many has no such column on either side.
|
|
1528
|
+
*
|
|
1529
|
+
* Sent rather than derived, for the same reason as `shape`: finding it means
|
|
1530
|
+
* pairing the two halves of the relation, and a rule implemented twice is a
|
|
1531
|
+
* rule that will eventually disagree with itself.
|
|
1532
|
+
*/
|
|
1533
|
+
readonly targetForeignKey?: string;
|
|
1534
|
+
}
|
|
1535
|
+
interface FieldDto {
|
|
1536
|
+
readonly name: string;
|
|
1537
|
+
readonly kind: FieldKindDto;
|
|
1538
|
+
/** Part of the model's primary key. */
|
|
1539
|
+
readonly isId: boolean;
|
|
1540
|
+
readonly isRequired: boolean;
|
|
1541
|
+
readonly isUnique: boolean;
|
|
1542
|
+
readonly isList: boolean;
|
|
1543
|
+
/**
|
|
1544
|
+
* Produced by the database or ORM (`cuid()`, `now()`, `autoincrement()`,
|
|
1545
|
+
* `@updatedAt`). Display it; do not ask the user for it.
|
|
1546
|
+
*/
|
|
1547
|
+
readonly isGenerated: boolean;
|
|
1548
|
+
/**
|
|
1549
|
+
* Literal default to pre-fill on create. Present only for editable fields
|
|
1550
|
+
* that declare one - a generated value has no literal to pre-fill.
|
|
1551
|
+
*/
|
|
1552
|
+
readonly defaultValue?: unknown;
|
|
1553
|
+
/** Present when `kind` is `'enum'`. */
|
|
1554
|
+
readonly enumValues?: readonly string[];
|
|
1555
|
+
/**
|
|
1556
|
+
* What to call the field, when the column name is not what people call it.
|
|
1557
|
+
*
|
|
1558
|
+
* Absent unless the application said so. A client falls back to `name`.
|
|
1559
|
+
*/
|
|
1560
|
+
readonly label?: string;
|
|
1561
|
+
/**
|
|
1562
|
+
* How the field should be edited, when its kind does not say enough.
|
|
1563
|
+
*
|
|
1564
|
+
* A `string` column may be a sentence, a password or a colour, and the schema
|
|
1565
|
+
* cannot tell them apart.
|
|
1566
|
+
*/
|
|
1567
|
+
readonly widget?: 'textarea' | 'password' | 'email' | 'url' | 'color' | 'json';
|
|
1568
|
+
/**
|
|
1569
|
+
* The admin will refuse to write this field.
|
|
1570
|
+
*
|
|
1571
|
+
* True for generated columns, and for anything the application marked
|
|
1572
|
+
* read-only. Enforced: a write naming it is rejected, so a client that
|
|
1573
|
+
* ignores this gets a 400 rather than a surprise.
|
|
1574
|
+
*/
|
|
1575
|
+
readonly readOnly: boolean;
|
|
1576
|
+
/**
|
|
1577
|
+
* The admin accepts this field on a write and never sends it back.
|
|
1578
|
+
*
|
|
1579
|
+
* Sent so the interface knows the blank it shows is not the stored value. A
|
|
1580
|
+
* password field that looked empty because the record had none would be a
|
|
1581
|
+
* different thing entirely.
|
|
1582
|
+
*/
|
|
1583
|
+
readonly writeOnly?: boolean;
|
|
1584
|
+
/** Present when `kind` is `relation`. */
|
|
1585
|
+
readonly relation?: RelationDto;
|
|
1586
|
+
}
|
|
1587
|
+
/** Which operations a principal may perform on one model. */
|
|
1588
|
+
interface ModelPermissionsDto {
|
|
1589
|
+
readonly list: boolean;
|
|
1590
|
+
readonly read: boolean;
|
|
1591
|
+
readonly create: boolean;
|
|
1592
|
+
readonly update: boolean;
|
|
1593
|
+
readonly delete: boolean;
|
|
1594
|
+
}
|
|
1595
|
+
/** An application-defined button the interface should draw. */
|
|
1596
|
+
interface ActionDto {
|
|
1597
|
+
readonly name: string;
|
|
1598
|
+
readonly label: string;
|
|
1599
|
+
readonly scope: 'record' | 'list';
|
|
1600
|
+
/** Ask this before running. Absent means run straight away. */
|
|
1601
|
+
readonly confirm?: string;
|
|
1602
|
+
/** Draw it as destructive. */
|
|
1603
|
+
readonly danger?: boolean;
|
|
1604
|
+
}
|
|
1605
|
+
interface ModelDto {
|
|
1606
|
+
readonly name: string;
|
|
1607
|
+
/** Field names forming the primary key. Single-column in this version. */
|
|
1608
|
+
readonly primaryKey: readonly string[];
|
|
1609
|
+
readonly fields: readonly FieldDto[];
|
|
1610
|
+
/**
|
|
1611
|
+
* Field that names a record of this model in one line.
|
|
1612
|
+
*
|
|
1613
|
+
* Sent rather than left for the UI to guess, because the guess would have to
|
|
1614
|
+
* match what the adapter already selected when it loaded the relation. Both
|
|
1615
|
+
* come from one rule in Core, so they cannot disagree.
|
|
1616
|
+
*/
|
|
1617
|
+
readonly displayField: string;
|
|
1618
|
+
/**
|
|
1619
|
+
* What this principal may do with the model.
|
|
1620
|
+
*
|
|
1621
|
+
* Sent so the interface can stop offering actions that will be refused. It is
|
|
1622
|
+
* a description of the policy's answers, not the enforcement: every request is
|
|
1623
|
+
* checked again when it arrives, and a client that ignores this gets a 403
|
|
1624
|
+
* rather than access.
|
|
1625
|
+
*
|
|
1626
|
+
* `metadata` is not among them - a model the principal cannot see is absent
|
|
1627
|
+
* from this document entirely.
|
|
1628
|
+
*/
|
|
1629
|
+
readonly can: ModelPermissionsDto;
|
|
1630
|
+
/**
|
|
1631
|
+
* Application-defined actions this principal may run.
|
|
1632
|
+
*
|
|
1633
|
+
* Already filtered by the policy: an action that would be refused is absent,
|
|
1634
|
+
* so the interface never draws a button that cannot work.
|
|
1635
|
+
*/
|
|
1636
|
+
readonly actions: readonly ActionDto[];
|
|
1637
|
+
/** What to call the model. Absent unless the application said so. */
|
|
1638
|
+
readonly label?: string;
|
|
1639
|
+
/**
|
|
1640
|
+
* Which icon to draw beside it in the navigation.
|
|
1641
|
+
*
|
|
1642
|
+
* One of a closed set the interface knows how to render - see `ModelIcon` in
|
|
1643
|
+
* Core. Absent unless the application named one, and absent is a real answer:
|
|
1644
|
+
* the same icon repeated down a column is decoration.
|
|
1645
|
+
*/
|
|
1646
|
+
readonly icon?: ModelIcon;
|
|
1647
|
+
}
|
|
1648
|
+
interface MetadataDto {
|
|
1649
|
+
readonly models: readonly ModelDto[];
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
export { type ActionDto, AdapterError, type AdminAccount, type AdminAccountStore, type AdminAccountSummary, type AdminAction, type AdminActionResult, type AdminActionsByModel, type AdminAuth, type AdminDashboard, type AdminErrorCode, type AdminErrorKind, type AdminHookContext, type AdminHooks, type AdminHooksByModel, AdminModule, type AdminModuleAsyncOptions, type AdminModuleFactoryOptions, type AdminModuleOptions, type AdminModuleOptionsFactory, type AdminOperation, type AdminResourceAuth, type AdminResponse, type BuiltInAuthOptions, type ChartWidget, ConstraintError, type ConstraintKind, type CountWidget, type DashboardWidget, type ErrorResponse, type FieldDto, type FieldKind, type FieldKindDto, type FieldMetadata, FieldNotFoundError, type FieldOverride, type FieldWidget, type FilterOperator, type FilterRule, ForbiddenError, InvalidQueryError, type ListQuery, type ListWidget, type MetadataDto, type ModelDto, type ModelMetadata, ModelNotFoundError, type ModelOverride, type ModelOverrides, NestAdminError, type OrmAdapter, type Page, type PageMeta, type RecordData, type RecordId, RecordNotFoundError, type RelationCardinality, type RelationDto, type RelationMetadata, type ResourceAuthorization, type SortDirection, type SortRule, type StatResult, type StatWidget, type SuccessResponse, UnauthorizedError, ValidationError, type WidgetSpan, adminAccountOf, builtInAuth, generateSessionSecret, hashAdminPassword, isNestAdminError, unsafeAllowAllRequests, verifyAdminPassword };
|