@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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalised, ORM-independent description of a model and its fields.
|
|
3
|
+
*
|
|
4
|
+
* Every ORM adapter translates its own schema representation (Prisma DMMF,
|
|
5
|
+
* TypeORM entity metadata, a Drizzle table object, ...) into these shapes.
|
|
6
|
+
* Nothing downstream - the CRUD engine, the HTTP API, the admin UI - is
|
|
7
|
+
* allowed to look at anything else.
|
|
8
|
+
*
|
|
9
|
+
* @experimental Draft contract. Expected to change during MVP implementation.
|
|
10
|
+
*/
|
|
11
|
+
/** ORM-independent classification of a scalar or relation field. */
|
|
12
|
+
type FieldKind = 'string' | 'number' | 'boolean' | 'datetime' | 'enum' | 'json' | 'relation'
|
|
13
|
+
/** The adapter recognised the field but cannot map it onto a known kind. */
|
|
14
|
+
| 'unknown';
|
|
15
|
+
/** Cardinality of a relation from the owning model's point of view. */
|
|
16
|
+
type RelationCardinality = 'one' | 'many';
|
|
17
|
+
/**
|
|
18
|
+
* A relation, and how to act on it.
|
|
19
|
+
*
|
|
20
|
+
* `from` and `to` are what turn a relation from something an admin can only
|
|
21
|
+
* display into something it can filter and write. A to-one relation is stored
|
|
22
|
+
* as an ordinary scalar column - `Post.authorId` - and that column is what a
|
|
23
|
+
* query has to be expressed in terms of. Without knowing its name, a filter on
|
|
24
|
+
* `author` cannot be translated, and a form has no field to submit.
|
|
25
|
+
*
|
|
26
|
+
* Both are absent on to-many relations, which have no column on this side.
|
|
27
|
+
*/
|
|
28
|
+
interface RelationMetadata {
|
|
29
|
+
/** `name` of the {@link ModelMetadata} on the other side of the relation. */
|
|
30
|
+
readonly targetModel: string;
|
|
31
|
+
readonly cardinality: RelationCardinality;
|
|
32
|
+
/**
|
|
33
|
+
* Scalar field on **this** model holding the foreign key, for a to-one
|
|
34
|
+
* relation - `authorId` on `Post.author`.
|
|
35
|
+
*
|
|
36
|
+
* Absent when the relation has no column on this side: every to-many, and
|
|
37
|
+
* the non-owning half of a one-to-one.
|
|
38
|
+
*/
|
|
39
|
+
readonly from?: string;
|
|
40
|
+
/** Field on the target model that `from` points at - usually its id. */
|
|
41
|
+
readonly to?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Name shared by both halves of the relation.
|
|
44
|
+
*
|
|
45
|
+
* The only reliable way to pair `User.posts` with `Post.author`, which two
|
|
46
|
+
* things need. Distinguishing a many-to-many from a one-to-many requires
|
|
47
|
+
* looking at the other side - both are `'many'` from here, but only one has
|
|
48
|
+
* no column anywhere. And knowing whether a child's key is required decides
|
|
49
|
+
* whether it can be detached at all.
|
|
50
|
+
*
|
|
51
|
+
* Two relations between the same pair of models are told apart by it too:
|
|
52
|
+
* `Post.author` and `Post.reviewer` both target `User`.
|
|
53
|
+
*/
|
|
54
|
+
readonly name?: string;
|
|
55
|
+
}
|
|
56
|
+
interface FieldMetadata {
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly kind: FieldKind;
|
|
59
|
+
/** Part of the model's primary key. */
|
|
60
|
+
readonly isId: boolean;
|
|
61
|
+
readonly isRequired: boolean;
|
|
62
|
+
readonly isUnique: boolean;
|
|
63
|
+
/** The field holds a list of {@link FieldKind} values. */
|
|
64
|
+
readonly isList: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The value is produced by the database or the ORM and is not asked of the
|
|
67
|
+
* user - `@default(cuid())`, `@default(now())`, `@default(autoincrement())`,
|
|
68
|
+
* `@updatedAt`. Such fields are displayed but not editable.
|
|
69
|
+
*
|
|
70
|
+
* This is NOT "has a default". A field with a literal default
|
|
71
|
+
* (`active Boolean @default(true)`) is an ordinary editable field that
|
|
72
|
+
* happens to arrive pre-filled; see {@link FieldMetadata.defaultValue}.
|
|
73
|
+
*
|
|
74
|
+
* NAME COLLISION - read before implementing an adapter. Prisma's DMMF also
|
|
75
|
+
* has a field called `isGenerated`, and it does NOT mean this. Measured
|
|
76
|
+
* against Prisma 7.10.0, DMMF reports `isGenerated: false` for
|
|
77
|
+
* `id String @id @default(cuid())`. Mapping it across directly produces
|
|
78
|
+
* editable primary keys. The correct derivation - a *function* default, or
|
|
79
|
+
* an updated-at column - is in `packages/prisma/src/metadata/to-metadata.ts`
|
|
80
|
+
* and `packages/drizzle/src/metadata/to-metadata.ts`, which state it in each
|
|
81
|
+
* ORM's own terms.
|
|
82
|
+
*/
|
|
83
|
+
readonly isGenerated: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Accepted on a write, never returned on a read.
|
|
86
|
+
*
|
|
87
|
+
* Set by `writeOnly` in the configuration. A password is the reason it
|
|
88
|
+
* exists: it has to be typed into a form and must never come back out, and
|
|
89
|
+
* `hidden` cannot express that - it refuses the field in both directions, so
|
|
90
|
+
* a hidden password column leaves no way to set one.
|
|
91
|
+
*
|
|
92
|
+
* Enforced twice, deliberately: the field is left out of the columns the
|
|
93
|
+
* adapter is asked for, *and* out of the projection applied to whatever comes
|
|
94
|
+
* back. One of those is enough; two is what it takes for a future adapter
|
|
95
|
+
* that ignores the field scope not to become a leak.
|
|
96
|
+
*/
|
|
97
|
+
readonly writeOnly?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Literal default the admin should pre-fill on create, when the schema
|
|
100
|
+
* declares one (`@default(true)`, `@default(0)`, `@default("USER")`).
|
|
101
|
+
*
|
|
102
|
+
* Absent for generated values: there is no literal to pre-fill for
|
|
103
|
+
* `@default(now())`, and {@link FieldMetadata.isGenerated} is `true` instead.
|
|
104
|
+
*/
|
|
105
|
+
readonly defaultValue?: unknown;
|
|
106
|
+
/** Populated when `kind` is `'enum'`. */
|
|
107
|
+
readonly enumValues?: readonly string[];
|
|
108
|
+
/** Populated when `kind` is `'relation'`. */
|
|
109
|
+
readonly relation?: RelationMetadata;
|
|
110
|
+
}
|
|
111
|
+
interface ModelMetadata {
|
|
112
|
+
/** Adapter-facing identifier, e.g. the Prisma model name `User`. */
|
|
113
|
+
readonly name: string;
|
|
114
|
+
/**
|
|
115
|
+
* Field names forming the primary key. Modelled as a list rather than a
|
|
116
|
+
* single `id` so composite keys do not require a breaking change later,
|
|
117
|
+
* even though the MVP will only support single-column keys.
|
|
118
|
+
*/
|
|
119
|
+
readonly primaryKey: readonly string[];
|
|
120
|
+
readonly fields: readonly FieldMetadata[];
|
|
121
|
+
/**
|
|
122
|
+
* Field that names a record of this model in one line, when the application
|
|
123
|
+
* has declared one.
|
|
124
|
+
*
|
|
125
|
+
* A slot rather than a value: left unset, `displayFieldFor` works it out from
|
|
126
|
+
* the fields. It is here so that a declared choice travels with the model and
|
|
127
|
+
* reaches the adapter and the metadata document alike, without either of them
|
|
128
|
+
* having to read configuration.
|
|
129
|
+
*/
|
|
130
|
+
readonly displayField?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* ORM-independent query description.
|
|
135
|
+
*
|
|
136
|
+
* The admin UI and the HTTP layer speak only this vocabulary; each adapter is
|
|
137
|
+
* responsible for translating it into its own query language.
|
|
138
|
+
*
|
|
139
|
+
* @experimental Draft contract. Expected to change during MVP implementation.
|
|
140
|
+
*/
|
|
141
|
+
type SortDirection = 'asc' | 'desc';
|
|
142
|
+
interface SortRule {
|
|
143
|
+
readonly field: string;
|
|
144
|
+
readonly direction: SortDirection;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The deliberately small operator set the MVP targets. Anything richer
|
|
148
|
+
* (nested relation filters, OR/AND trees, full-text) is a later concern and
|
|
149
|
+
* should extend this union rather than bypass it.
|
|
150
|
+
*/
|
|
151
|
+
type FilterOperator = 'eq' | 'ne' | 'contains' | 'startsWith' | 'endsWith' | 'gt' | 'gte' | 'lt' | 'lte' | 'in';
|
|
152
|
+
interface FilterRule {
|
|
153
|
+
readonly field: string;
|
|
154
|
+
readonly operator: FilterOperator;
|
|
155
|
+
readonly value: unknown;
|
|
156
|
+
}
|
|
157
|
+
/** Page-number based pagination. Cursor pagination is a later addition. */
|
|
158
|
+
interface ListQuery {
|
|
159
|
+
readonly page?: number;
|
|
160
|
+
readonly perPage?: number;
|
|
161
|
+
readonly sort?: readonly SortRule[];
|
|
162
|
+
readonly filters?: readonly FilterRule[];
|
|
163
|
+
/** Free-text term the adapter applies across searchable string fields. */
|
|
164
|
+
readonly search?: string;
|
|
165
|
+
/**
|
|
166
|
+
* The fields this query may touch, and the only ones it should return.
|
|
167
|
+
*
|
|
168
|
+
* Set by the caller that knows which fields the admin exposes - the adapter
|
|
169
|
+
* reads a schema, not a configuration. Without it, a field the application
|
|
170
|
+
* hid would still be searched by free text, sortable, filterable and
|
|
171
|
+
* returned, because from the adapter's side it is an ordinary column.
|
|
172
|
+
*
|
|
173
|
+
* Omitted means "every field the model has".
|
|
174
|
+
*/
|
|
175
|
+
readonly fields?: readonly string[];
|
|
176
|
+
}
|
|
177
|
+
interface Page<T> {
|
|
178
|
+
readonly data: readonly T[];
|
|
179
|
+
readonly total: number;
|
|
180
|
+
readonly page: number;
|
|
181
|
+
readonly perPage: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The single seam between Nest Admin and any ORM.
|
|
186
|
+
*
|
|
187
|
+
* Adding support for a new ORM means writing one implementation of
|
|
188
|
+
* {@link OrmAdapter} and nothing else. Core, the NestJS integration, the HTTP
|
|
189
|
+
* contract and the admin UI stay untouched.
|
|
190
|
+
*
|
|
191
|
+
* @experimental Draft contract. Expected to change during MVP implementation.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Primary key value of a single record. Composite keys are represented by
|
|
196
|
+
* {@link ModelMetadata.primaryKey}; supporting them at this level is a
|
|
197
|
+
* post-MVP change.
|
|
198
|
+
*/
|
|
199
|
+
type RecordId = string | number;
|
|
200
|
+
/** An untyped record as it crosses the adapter boundary. */
|
|
201
|
+
type RecordData = Record<string, unknown>;
|
|
202
|
+
interface OrmAdapter {
|
|
203
|
+
/** Stable identifier used in diagnostics, e.g. `'prisma'`. */
|
|
204
|
+
readonly name: string;
|
|
205
|
+
/**
|
|
206
|
+
* Discover the models the adapter can serve. Asynchronous because an adapter
|
|
207
|
+
* may need to read a schema file or import a generated client.
|
|
208
|
+
*/
|
|
209
|
+
getModels(): Promise<readonly ModelMetadata[]>;
|
|
210
|
+
list(model: string, query: ListQuery): Promise<Page<RecordData>>;
|
|
211
|
+
findOne(model: string, id: RecordId): Promise<RecordData | null>;
|
|
212
|
+
create(model: string, data: RecordData): Promise<RecordData>;
|
|
213
|
+
update(model: string, id: RecordId, data: RecordData): Promise<RecordData>;
|
|
214
|
+
delete(model: string, id: RecordId): Promise<void>;
|
|
215
|
+
/**
|
|
216
|
+
* A page of the records on the far side of a to-many relation.
|
|
217
|
+
*
|
|
218
|
+
* Paginated for the same reason a list is: the number of children is a
|
|
219
|
+
* property of the data, not of the schema, and a parent with fifty thousand
|
|
220
|
+
* of them must not be a page that never loads.
|
|
221
|
+
*
|
|
222
|
+
* Kept separate from `list` rather than expressed as a filter because a
|
|
223
|
+
* many-to-many has no column to filter on - the link lives in a join table.
|
|
224
|
+
* A one-to-many could be asked for either way; going through one method means
|
|
225
|
+
* the caller does not have to know which it is looking at.
|
|
226
|
+
*/
|
|
227
|
+
listRelated(model: string, id: RecordId, relationField: string, query: ListQuery): Promise<Page<RecordData>>;
|
|
228
|
+
/**
|
|
229
|
+
* Link an existing record to this one.
|
|
230
|
+
*
|
|
231
|
+
* Across a many-to-many this adds a row to the join table and changes
|
|
232
|
+
* neither record. Across a one-to-many it rewrites the child's foreign key,
|
|
233
|
+
* which also **removes it from whatever parent held it** - the same operation
|
|
234
|
+
* with a consequence the caller should have been told about. Deciding whether
|
|
235
|
+
* to warn is the transport layer's job; the adapter performs what it is asked.
|
|
236
|
+
*/
|
|
237
|
+
attachRelated(model: string, id: RecordId, relationField: string, targetId: RecordId): Promise<void>;
|
|
238
|
+
/**
|
|
239
|
+
* Unlink a record from this one, without deleting either.
|
|
240
|
+
*
|
|
241
|
+
* Across a one-to-many this clears the child's foreign key, which is
|
|
242
|
+
* impossible when that column is required - see `detachBlockedReason`. The
|
|
243
|
+
* adapter may assume the caller has checked, and will surface the database's
|
|
244
|
+
* own refusal if it has not.
|
|
245
|
+
*/
|
|
246
|
+
detachRelated(model: string, id: RecordId, relationField: string, targetId: RecordId): Promise<void>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The Drizzle implementation of `OrmAdapter`.
|
|
251
|
+
*
|
|
252
|
+
* This package exists to answer a question the Prisma adapter cannot: is
|
|
253
|
+
* `OrmAdapter` a contract, or is it a description of Prisma? Writing a second
|
|
254
|
+
* implementation against a genuinely different ORM - a query builder with no
|
|
255
|
+
* generated client, no DMMF and no normalised errors - is the only way to find
|
|
256
|
+
* out before 1.0 freezes it.
|
|
257
|
+
*
|
|
258
|
+
* The answer, recorded here because it is the point of the package: Core needed
|
|
259
|
+
* no changes. What differs is entirely inside this directory, and each
|
|
260
|
+
* difference is documented where it is handled.
|
|
261
|
+
*
|
|
262
|
+
* ## What Drizzle does not give us, and what is done instead
|
|
263
|
+
*
|
|
264
|
+
* | Prisma | Drizzle | Handled in |
|
|
265
|
+
* | --- | --- | --- |
|
|
266
|
+
* | DMMF describing every model | the schema object itself | `schema/introspect.ts` |
|
|
267
|
+
* | `P2xxx` codes with `meta` | the driver's own error | `errors/constraints.ts` |
|
|
268
|
+
* | `mode: 'insensitive'` | `lower()` on both sides | `query/build.ts` |
|
|
269
|
+
* | escaped `contains` | escaped by hand | `query/build.ts` |
|
|
270
|
+
* | relations always named | named only if declared | `schema/introspect.ts` |
|
|
271
|
+
*
|
|
272
|
+
* ## Relations are not loaded with the record
|
|
273
|
+
*
|
|
274
|
+
* The Prisma adapter includes a to-one's target so a list can show a person's
|
|
275
|
+
* name rather than their id. Drizzle can do the same with a join, but only with
|
|
276
|
+
* the relational query API, which needs `relations()` declared - and this
|
|
277
|
+
* adapter deliberately works without them. So a to-one arrives as its foreign
|
|
278
|
+
* key, and the interface resolves the label through the relation picker, which
|
|
279
|
+
* it already does for every relation it cannot see inline.
|
|
280
|
+
*/
|
|
281
|
+
|
|
282
|
+
interface DrizzleAdapterOptions {
|
|
283
|
+
/** A constructed Drizzle database, from any dialect's `drizzle()`. */
|
|
284
|
+
readonly db: unknown;
|
|
285
|
+
/**
|
|
286
|
+
* The schema module.
|
|
287
|
+
*
|
|
288
|
+
* Passed separately from `db` even though `drizzle(client, { schema })` also
|
|
289
|
+
* takes it, because that form is optional and a database built without it
|
|
290
|
+
* carries nothing to introspect.
|
|
291
|
+
*/
|
|
292
|
+
readonly schema: Readonly<Record<string, unknown>>;
|
|
293
|
+
}
|
|
294
|
+
declare class DrizzleAdapter implements OrmAdapter {
|
|
295
|
+
#private;
|
|
296
|
+
readonly name = "drizzle";
|
|
297
|
+
constructor(options: DrizzleAdapterOptions);
|
|
298
|
+
getModels(): Promise<readonly ModelMetadata[]>;
|
|
299
|
+
list(model: string, query: ListQuery): Promise<Page<RecordData>>;
|
|
300
|
+
findOne(model: string, id: RecordId): Promise<RecordData | null>;
|
|
301
|
+
create(model: string, data: RecordData): Promise<RecordData>;
|
|
302
|
+
update(model: string, id: RecordId, data: RecordData): Promise<RecordData>;
|
|
303
|
+
delete(model: string, id: RecordId): Promise<void>;
|
|
304
|
+
listRelated(model: string, id: RecordId, relationField: string, query: ListQuery): Promise<Page<RecordData>>;
|
|
305
|
+
attachRelated(model: string, id: RecordId, relationField: string, targetId: RecordId): Promise<void>;
|
|
306
|
+
detachRelated(model: string, id: RecordId, relationField: string, targetId: RecordId): Promise<void>;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* `ListQuery` into a Drizzle query.
|
|
311
|
+
*
|
|
312
|
+
* The rules enforced here are the ones the Prisma adapter enforces, and they
|
|
313
|
+
* are enforced again rather than shared because they are about *this* ORM's
|
|
314
|
+
* capabilities: which fields can be filtered, which operators a kind admits,
|
|
315
|
+
* what a search box searches. Where the two adapters agree, they agree because
|
|
316
|
+
* Core's contract says the same thing to both.
|
|
317
|
+
*
|
|
318
|
+
* ## Two places Drizzle needs work Prisma did for us
|
|
319
|
+
*
|
|
320
|
+
* **Case insensitivity.** Prisma has `mode: 'insensitive'`, on the providers
|
|
321
|
+
* that support it. Drizzle has `ilike`, on Postgres only. Rather than branch per
|
|
322
|
+
* dialect, both sides of the comparison go through `lower()`, which every
|
|
323
|
+
* dialect this adapter supports has. It costs an index unless one is declared on
|
|
324
|
+
* the expression - noted here because that is a real trade and not a free one.
|
|
325
|
+
*
|
|
326
|
+
* **`LIKE` metacharacters.** Prisma escapes `%` and `_` inside `contains`.
|
|
327
|
+
* Building the pattern by hand means doing it here, or a search for `100%`
|
|
328
|
+
* silently matches every row.
|
|
329
|
+
*/
|
|
330
|
+
|
|
331
|
+
/** Kept in step with `MAX_PER_PAGE` in the Prisma adapter and the UI's page-size list. */
|
|
332
|
+
declare const DEFAULT_PER_PAGE = 25;
|
|
333
|
+
declare const MAX_PER_PAGE = 100;
|
|
334
|
+
|
|
335
|
+
export { DEFAULT_PER_PAGE, DrizzleAdapter, type DrizzleAdapterOptions, MAX_PER_PAGE };
|