@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nest Admin contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # @nest-admin/nestjs
2
+
3
+ An admin panel for NestJS applications, generated from your ORM schema.
4
+
5
+ Add one module to an existing application and get list, create, read, update and
6
+ delete screens for every model — with search, filters, sorting, pagination,
7
+ relation pickers, a dashboard and a login page. No generated files, no
8
+ scaffolding to maintain, and no build step in your project: the interface ships
9
+ built, inside this package.
10
+
11
+ ```bash
12
+ npm install @nest-admin/nestjs
13
+ ```
14
+
15
+ ```ts
16
+ import { Module } from '@nestjs/common'
17
+ import { AdminModule, unsafeAllowAllRequests } from '@nest-admin/nestjs'
18
+ import { PrismaAdapter } from '@nest-admin/nestjs/prisma'
19
+
20
+ @Module({
21
+ imports: [
22
+ AdminModule.forRoot({
23
+ adapter: new PrismaAdapter({ client: prisma }),
24
+ auth: unsafeAllowAllRequests(), // development only - see below
25
+ }),
26
+ ],
27
+ })
28
+ export class AppModule {}
29
+ ```
30
+
31
+ Open `/admin`.
32
+
33
+ Node ≥ 20.11 · NestJS 10–12 · Prisma ≥ 6 or Drizzle ≥ 0.44
34
+
35
+ ---
36
+
37
+ ## Two ORMs, one contract
38
+
39
+ The adapter is a subpath, so an application that never imports one never loads
40
+ its code:
41
+
42
+ ```ts
43
+ import { PrismaAdapter } from '@nest-admin/nestjs/prisma'
44
+ new PrismaAdapter({ client: prisma })
45
+
46
+ import { DrizzleAdapter } from '@nest-admin/nestjs/drizzle'
47
+ new DrizzleAdapter({ db, schema }) // pass the schema module itself
48
+ ```
49
+
50
+ Everything above the adapter is identical either way — the HTTP layer and the
51
+ interface have no knowledge of any ORM, and that is enforced by tests rather
52
+ than by discipline.
53
+
54
+ ## Authentication is required
55
+
56
+ The admin exposes every record and, through `/admin/meta`, the whole schema. It
57
+ is never public by default.
58
+
59
+ **If your application already has identity**, supply the decision:
60
+
61
+ ```ts
62
+ import { ForbiddenError, UnauthorizedError, type AdminAuth } from '@nest-admin/nestjs'
63
+
64
+ const auth: AdminAuth = {
65
+ authorize(context) {
66
+ const request = context.switchToHttp().getRequest()
67
+ if (!request.user) throw new UnauthorizedError('Sign in first.')
68
+ if (!request.user.isStaff) throw new ForbiddenError('Staff only.')
69
+ },
70
+ }
71
+ ```
72
+
73
+ | Outcome | How | HTTP |
74
+ | --------------------------- | ---------------------------- | ---: |
75
+ | Allow | return (or resolve) normally | 2xx |
76
+ | No identity | throw `UnauthorizedError` | 401 |
77
+ | Identity, but not permitted | throw `ForbiddenError` | 403 |
78
+ | Denied without saying which | return `false` | 403 |
79
+
80
+ `authorize` may be sync or async and receives the NestJS `ExecutionContext`, so
81
+ you can read whatever principal your own middleware attached. Throwing is
82
+ preferred: `false` cannot express the 401/403 distinction, and a client can act
83
+ on "sign in" but not on "denied". If your auth code throws anything else the
84
+ request is **refused** with a generic 500 — a bug in authentication never
85
+ becomes an accidental allow.
86
+
87
+ **If it does not**, `builtInAuth()` provides a login screen, a signed session
88
+ cookie and scrypt password hashing:
89
+
90
+ ```ts
91
+ import { builtInAuth } from '@nest-admin/nestjs'
92
+ import { prismaAccountStore } from '@nest-admin/nestjs/prisma'
93
+
94
+ auth: builtInAuth({
95
+ store: prismaAccountStore({ client: prisma }),
96
+ session: { secret: process.env.ADMIN_SESSION_SECRET },
97
+ })
98
+ ```
99
+
100
+ Its accounts live in a table of their own, **not** your `User` table. The people
101
+ who administer a system are not rows in the table they administer.
102
+
103
+ For local development only, `unsafeAllowAllRequests()` makes the whole admin
104
+ public and logs a warning on every startup.
105
+
106
+ ## Per-model authorization
107
+
108
+ Optional. Omitting it permits every model, which is not a hole — `auth` already
109
+ gates entry.
110
+
111
+ ```ts
112
+ resourceAuth: {
113
+ authorize({ context, model, operation }) {
114
+ const { user } = context.switchToHttp().getRequest()
115
+ if (model === 'AuditLog') return user.isAdmin
116
+ if (operation === 'delete') return user.isAdmin
117
+ return true
118
+ },
119
+ }
120
+ ```
121
+
122
+ `operation` is one of `metadata`, `list`, `read`, `create`, `update`, `delete`.
123
+
124
+ | Operation | Denied means |
125
+ | ------------- | -------------------------------------------------------------- |
126
+ | `metadata` | the model is **omitted** from `GET /admin/meta` — not an error |
127
+ | anything else | `403 FORBIDDEN`, and the adapter is never called |
128
+
129
+ A model hidden from metadata also has any relation **pointing at it** removed
130
+ from the models that remain, so its name cannot leak through
131
+ `relation.targetModel`. There is no client-side hiding anywhere in the admin.
132
+
133
+ Row-level rules — "only their own orders" — do not exist yet.
134
+
135
+ ## What else you can configure
136
+
137
+ ```ts
138
+ resources: { exclude: ['AdminAccount'] }, // which models are part of the admin
139
+ models: { User: { label: 'People', fields: { bio: { widget: 'textarea' } } } },
140
+ hooks: { Post: { beforeCreate: ({ data }) => ({ ...data, slug: slugify(data.title) }) } },
141
+ actions: { Post: [{ name: 'publish', scope: 'record', run: async ({ id }) => ({ message: 'Done' }) }] },
142
+ dashboard: [{ kind: 'count', title: 'Customers', model: 'User', compareDays: 30 }],
143
+ theme: { title: 'Acme Admin', brandColor: '#3f6212' },
144
+ path: '/admin',
145
+ ```
146
+
147
+ Field options divide on a line worth knowing: `hidden`, `readOnly`, `writeOnly`
148
+ and `displayField` are **enforced by the server**; `label`, `widget`, `order`
149
+ and `icon` are presentation, sent to a client that could ignore them.
150
+
151
+ Full reference: [docs/configuration.md](../../docs/configuration.md).
152
+
153
+ ## The admin interface
154
+
155
+ The built interface ships inside this package. Two routes serve it, matched
156
+ **before** the API routes so `assets` is never read as a model name:
157
+
158
+ | Route | Serves |
159
+ | ------------------------- | -------------------------- |
160
+ | `GET /admin` | the SPA shell (`no-cache`) |
161
+ | `GET /admin/assets/:file` | hashed bundles (immutable) |
162
+
163
+ Routing is hash-based (`/admin#/User/u1`), so deep links are still requests for
164
+ `/admin` and no catch-all fallback is needed — a fallback would have to match
165
+ `/admin/*`, which is exactly the space the API occupies.
166
+
167
+ **The shell is served without authentication, deliberately.** It is a static
168
+ bundle: no records, no schema, identical for every visitor. It discovers what
169
+ exists by calling `/admin/meta`, which _is_ guarded. Guarding the shell would
170
+ render a JSON 401 in the browser instead of a page that can explain itself.
171
+
172
+ The mount path is configurable; the bundle is rewritten to match as it is
173
+ served, so `path: '/backoffice'` needs no rebuild.
174
+
175
+ ## HTTP contract
176
+
177
+ | Method | Route | Purpose |
178
+ | -------- | --------------------------------------- | ----------------------------------------------- |
179
+ | `GET` | `/admin/meta` | Models and fields, filtered by what you may see |
180
+ | `GET` | `/admin/dashboard` | The landing page's widgets, already resolved |
181
+ | `GET` | `/admin/:model` | List records |
182
+ | `GET` | `/admin/:model/:id` | Read one |
183
+ | `POST` | `/admin/:model` | Create |
184
+ | `PATCH` | `/admin/:model/:id` | Update |
185
+ | `DELETE` | `/admin/:model/:id` | Delete |
186
+ | `DELETE` | `/admin/:model` | Delete several |
187
+ | `GET` | `/admin/:model/:id/:relation` | A page of related records |
188
+ | `POST` | `/admin/:model/:id/:relation` | Attach |
189
+ | `DELETE` | `/admin/:model/:id/:relation/:targetId` | Detach |
190
+ | `POST` | `/admin/actions/:model/:action` | Run a declared action |
191
+
192
+ `:model` is the model name exactly as the schema declares it. Under Drizzle that
193
+ is the key you exported the table under.
194
+
195
+ ### Query syntax
196
+
197
+ Only `page`, `perPage`, `search`, `sort` and `filter` are accepted. Anything
198
+ else is a `400` — including bracket syntax (`?filter[age][gte]=18`), which used
199
+ to be ignored, so a caller believed it had filtered and received every record.
200
+
201
+ ```text
202
+ ?page=2&perPage=25&search=ada
203
+ ?sort=email:asc&sort=createdAt:desc
204
+ ?filter=age:gte:18&filter=role:in:ADMIN,USER
205
+ ```
206
+
207
+ `sort` and `filter` are repeatable and order is preserved. A filter is
208
+ `field:operator:value`, split into at most three parts so colons inside a value
209
+ survive. Operators: `eq`, `ne`, `contains`, `startsWith`, `endsWith`, `gt`,
210
+ `gte`, `lt`, `lte`, `in`. Values are coerced from the field's declared type, so
211
+ `age:gte:30` reaches the ORM as a number and `active:eq:true` as a boolean.
212
+
213
+ ### Response envelope
214
+
215
+ ```jsonc
216
+ // success
217
+ { "success": true, "data": {}, "meta": { "total": 3, "page": 1, "perPage": 25 } }
218
+
219
+ // failure
220
+ { "success": false, "error": { "code": "MODEL_NOT_FOUND", "message": "…", "details": {} } }
221
+ ```
222
+
223
+ `meta` is present on list responses only.
224
+
225
+ ### Errors
226
+
227
+ | Code | Status |
228
+ | -------------------------------------------------------- | -----: |
229
+ | `UNAUTHORIZED` | 401 |
230
+ | `FORBIDDEN` | 403 |
231
+ | `MODEL_NOT_FOUND` / `RECORD_NOT_FOUND` | 404 |
232
+ | `FIELD_NOT_FOUND` / `INVALID_QUERY` / `VALIDATION_ERROR` | 400 |
233
+ | `CONSTRAINT_VIOLATION` | 409 |
234
+ | `INTERNAL_ERROR` | 500 |
235
+
236
+ Only those messages are forwarded. Everything else becomes a generic 500 and is
237
+ logged server-side: an ORM's own message carries call sites, filesystem paths
238
+ and sometimes the submitted data, and none of that belongs in a browser.
239
+ `CONSTRAINT_VIOLATION` carries `details.fields`, which is how "that email is
240
+ taken" lands under the email box rather than in a banner.
241
+
242
+ ## Documentation
243
+
244
+ - [Getting started](../../docs/getting-started.md)
245
+ - [Configuration reference](../../docs/configuration.md)
246
+ - [Adapters, and writing one](../../docs/adapters.md)
247
+ - [Architecture](../../docs/architecture.md)
248
+ - [What exists and what does not](../../docs/status.md)
249
+
250
+ ## License
251
+
252
+ MIT