@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/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
|