nuxt-auto-crud 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -360
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -0
- package/dist/runtime/server/api/[model]/[id].patch.js +3 -2
- package/dist/runtime/server/api/[model]/index.post.js +3 -2
- package/dist/runtime/server/api/_meta.get.d.ts +21 -0
- package/dist/runtime/server/api/_meta.get.js +91 -0
- package/dist/runtime/server/api/_schema/[table].get.d.ts +1 -0
- package/dist/runtime/server/utils/auth.js +19 -6
- package/dist/runtime/server/utils/constants.d.ts +2 -0
- package/dist/runtime/server/utils/constants.js +36 -0
- package/dist/runtime/server/utils/modelMapper.d.ts +7 -65
- package/dist/runtime/server/utils/modelMapper.js +27 -15
- package/dist/runtime/server/utils/schema.d.ts +2 -0
- package/dist/runtime/server/utils/schema.js +4 -0
- package/package.json +3 -2
- package/src/runtime/server/api/[model]/[id].patch.ts +3 -2
- package/src/runtime/server/api/[model]/index.post.ts +5 -4
- package/src/runtime/server/api/_meta.get.ts +111 -0
- package/src/runtime/server/utils/auth.ts +29 -4
- package/src/runtime/server/utils/constants.ts +25 -0
- package/src/runtime/server/utils/modelMapper.ts +45 -111
- package/src/runtime/server/utils/schema.ts +8 -0
package/README.md
CHANGED
|
@@ -1,373 +1,43 @@
|
|
|
1
1
|
# Nuxt Auto CRUD
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Nuxt Auto CRUD is a headless, zero-codegen CRUD engine that transforms Drizzle ORM schemas into fully functional RESTful APIs for Nuxt 4.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
| Specification | Details |
|
|
6
|
+
| :--- | :--- |
|
|
7
|
+
| **Runtime** | Nuxt 4 (`app/` directory), Nitro |
|
|
8
|
+
| **Persistence** | SQLite / libSQL (Optimized for Cloudflare D1) |
|
|
9
|
+
| **ORM & SSOT** | Drizzle ORM (Schema-driven) |
|
|
10
|
+
| **Validation** | `drizzle-zod` (Dynamic derivation) |
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
## 🛠 Architectural Logic: Zero-Codegen
|
|
13
|
+
NAC treats your Drizzle schema as the **Single Source of Truth (SSOT)**. Unlike traditional scaffolds, it does not generate physical files; it mounts dynamic Nitro handlers at runtime.
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
* **Dynamic Routing**: Automatically maps `GET|POST|PATCH|DELETE` to your Drizzle tables.
|
|
16
|
+
* **Agentic Compatibility**: Built with an MCP-friendly structure to allow AI Agents to interact directly with the schema-driven API.
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
## 🔐 RBAC & Permissions
|
|
19
|
+
Integrates with `nuxt-authorization` for database-driven Role-Based Access Control.
|
|
20
|
+
* **Ownership Logic**: Supports `update_own` and `delete_own` via `createdBy` column reflection.
|
|
21
|
+
* **Granular Scopes**: Fine-grained control over `list` vs `list_all` (drafts/soft-deleted).
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
## 🌐 Endpoints
|
|
24
|
+
| Method | Endpoint | Description |
|
|
25
|
+
| :--- | :--- | :--- |
|
|
26
|
+
| `GET` | `/api/:model` | List with filtering/paging |
|
|
27
|
+
| `POST` | `/api/:model` | Validated creation |
|
|
28
|
+
| `GET` | `/api/:model/:id` | Single record retrieval |
|
|
29
|
+
| `PATCH` | `/api/:model/:id` | Partial validated update |
|
|
30
|
+
| `DELETE` | `/api/:model/:id` | Soft/Hard deletion |
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
---
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
## Installation
|
|
35
|
+
It is highly recommended to use the [Template](https://auto-crud.clifland.in/docs/auto-crud) for new installations.
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
- `POST /api/:model` - Create a new record
|
|
25
|
-
- `GET /api/:model/:id` - Get record by ID
|
|
26
|
-
- `PATCH /api/:model/:id` - Update record
|
|
27
|
-
- `DELETE /api/:model/:id` - Delete record
|
|
37
|
+
If you are adding it to an existing application, refer to the [Manual Installation](https://auto-crud.clifland.in/docs/manual-installation) guide.
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
[YouTube Walkthrough](https://www.youtube.com/watch?v=_o0cddJUU50&list=PLnbvxcojhIixqM1J08Tnm7vmMdx2wsy4B)
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
[NPM Package](https://www.npmjs.com/package/nuxt-auto-crud)
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npx nuxi init -t gh:clifordpereira/nuxt-auto-crud_template <project-name>
|
|
37
|
-
cd <project-name>
|
|
38
|
-
bun install
|
|
39
|
-
bun db:generate
|
|
40
|
-
bun run dev
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Template Usage Modes:**
|
|
44
|
-
|
|
45
|
-
1. **Fullstack App**: The template includes the `nuxt-auto-crud` module, providing both the backend APIs and the frontend UI. [Watch Demo](https://youtu.be/M9-koXmhB9k)
|
|
46
|
-
2. **Frontend Only**: You can use the template just for the frontend. In this case, you don't need to install the module in the frontend app. Instead, you would install `nuxt-auto-crud` in a separate backend setup (e.g., another Nuxt project acting as the API).
|
|
47
|
-
|
|
48
|
-
Detailed instructions can be found in [https://auto-crud.clifland.in/docs/auto-crud](https://auto-crud.clifland.in/docs/auto-crud)
|
|
49
|
-
|
|
50
|
-
### 2. Manual Setup (Existing Project)
|
|
51
|
-
|
|
52
|
-
If you want to add `nuxt-auto-crud` to an existing project, follow these steps:
|
|
53
|
-
|
|
54
|
-
> **Note:** These instructions have been simplified for NuxtHub.
|
|
55
|
-
|
|
56
|
-
#### Install dependencies
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
npm install nuxt-auto-crud @nuxthub/core@^0.10.0 drizzle-orm
|
|
60
|
-
npm install nuxt-auth-utils nuxt-authorization # Optional: for authentication
|
|
61
|
-
npm install --save-dev wrangler drizzle-kit
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
> You can also use `bun` or `pnpm` instead of `npm`.
|
|
65
|
-
|
|
66
|
-
#### Configure Nuxt
|
|
67
|
-
|
|
68
|
-
Add the modules to your `nuxt.config.ts`:
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
// nuxt.config.ts
|
|
72
|
-
export default defineNuxtConfig({
|
|
73
|
-
modules: ['@nuxthub/core', 'nuxt-auto-crud'],
|
|
74
|
-
|
|
75
|
-
hub: {
|
|
76
|
-
db: 'sqlite',
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
autoCrud: {
|
|
80
|
-
schemaPath: 'server/db/schema',
|
|
81
|
-
// auth: false,
|
|
82
|
-
auth: {
|
|
83
|
-
type: 'session', // for Normal Authentication with nuxt-auth-utils
|
|
84
|
-
authentication: true,
|
|
85
|
-
authorization: true,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
#### Configure Drizzle
|
|
92
|
-
|
|
93
|
-
Add the generation script to your `package.json`:
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"scripts": {
|
|
98
|
-
"db:generate": "nuxt db generate"
|
|
99
|
-
}
|
|
100
|
-
// ...
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Create `drizzle.config.ts` in your project root:
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// drizzle.config.ts
|
|
108
|
-
import { defineConfig } from 'drizzle-kit'
|
|
109
|
-
|
|
110
|
-
export default defineConfig({
|
|
111
|
-
dialect: 'sqlite',
|
|
112
|
-
schema: './server/db/schema/index.ts', // Point to your schema index file
|
|
113
|
-
out: './server/db/migrations'
|
|
114
|
-
})
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
#### Define your database schema
|
|
120
|
-
|
|
121
|
-
Create your schema files in `server/db/schema/`. For example, `server/db/schema/users.ts`:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
// server/db/schema/users.ts
|
|
125
|
-
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
126
|
-
|
|
127
|
-
export const users = sqliteTable('users', {
|
|
128
|
-
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
129
|
-
name: text('name').notNull(),
|
|
130
|
-
email: text('email').notNull().unique(),
|
|
131
|
-
password: text('password').notNull(),
|
|
132
|
-
avatar: text('avatar').notNull(),
|
|
133
|
-
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
|
134
|
-
})
|
|
135
|
-
```
|
|
136
|
-
#### Run the project
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
cd <project-name>
|
|
140
|
-
bun db:generate
|
|
141
|
-
bun run dev
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
That's it! 🎉 Your CRUD APIs are now available at `/api/users`.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
#### Adding New Schemas
|
|
148
|
-
|
|
149
|
-
To add a new table (e.g., `posts`), simply create a new file in your schema directory:
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
// server/db/schema/posts.ts
|
|
153
|
-
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
154
|
-
import { users } from './users'
|
|
155
|
-
|
|
156
|
-
export const posts = sqliteTable('posts', {
|
|
157
|
-
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
158
|
-
title: text('title').notNull(),
|
|
159
|
-
content: text('content').notNull(),
|
|
160
|
-
authorId: integer('author_id').references(() => users.id),
|
|
161
|
-
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
|
162
|
-
})
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Then, ensure it is exported in your `server/db/schema/index.ts` (if you are using an index file) or that your `drizzle.config.ts` is pointing to the correct location.
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// server/db/schema/index.ts
|
|
169
|
-
export * from './users'
|
|
170
|
-
export * from './posts'
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
After adding the file, run the generation script:
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
bun db:generate
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
The new API endpoints (e.g., `/api/posts`) will be automatically available. [Watch Demo](https://youtu.be/7gW0KW1KtN0)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
> **Note:** The `organization.ts` and `cms.ts` files you might see in the playground are just examples and are commented out by default. You should implement a robust schema tailored to your production needs.
|
|
183
|
-
|
|
184
|
-
### 3. Backend-only App (API Mode)
|
|
185
|
-
|
|
186
|
-
If you are using Nuxt as a backend for a separate client application (e.g., mobile app, SPA), you can use this module to quickly expose REST APIs.
|
|
187
|
-
|
|
188
|
-
In this case, you might handle authentication differently (e.g., validating tokens in middleware) or disable the built-in auth checks if you have a global auth middleware.
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
// nuxt.config.ts
|
|
192
|
-
export default defineNuxtConfig({
|
|
193
|
-
modules: ['nuxt-auto-crud'],
|
|
194
|
-
autoCrud: {
|
|
195
|
-
schemaPath: 'server/db/schema',
|
|
196
|
-
// auth: false, // Uncomment this line for testing APIs without auth
|
|
197
|
-
auth: {
|
|
198
|
-
type: 'jwt', // for app providing backend apis only
|
|
199
|
-
authentication: true,
|
|
200
|
-
authorization: true,
|
|
201
|
-
jwtSecret: process.env.NUXT_JWT_SECRET || 'test-secret-key-123',
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
**Note:** Remember to add your `NUXT_JWT_SECRET` in `.env`.
|
|
208
|
-
|
|
209
|
-
You should also configure `drizzle.config.ts` correctly:
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
// drizzle.config.ts
|
|
213
|
-
import { defineConfig } from 'drizzle-kit'
|
|
214
|
-
|
|
215
|
-
export default defineConfig({
|
|
216
|
-
dialect: 'sqlite',
|
|
217
|
-
schema: './server/db/schema/index.ts',
|
|
218
|
-
out: './server/db/migrations',
|
|
219
|
-
tablesFilter: ['!_hub_migrations'],
|
|
220
|
-
})
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## 🔐 Authentication
|
|
224
|
-
|
|
225
|
-
Authentication is enabled by default using **Session Auth** (requires `nuxt-auth-utils` and `nuxt-authorization`).
|
|
226
|
-
|
|
227
|
-
To disable auth for testing:
|
|
228
|
-
```typescript
|
|
229
|
-
autoCrud: { auth: false }
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
For **JWT Auth** (backend-only apps) or advanced configuration, see the [Authentication docs](https://auto-crud.clifland.in/docs/configuration/authentication).
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
## 🛡️ Public View Configuration (Field Visibility)
|
|
237
|
-
|
|
238
|
-
You can define which fields are visible to unauthenticated (guest) users in your `nuxt.config.ts`.
|
|
239
|
-
> **Note:** Access control (RBAC) - determining *who* can access *what* - is expected to be handled by your database/permissions system (using `nuxt-authorization`). This configuration only controls the *serialization* of the response for guests.
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
// nuxt.config.ts
|
|
243
|
-
export default defineNuxtConfig({
|
|
244
|
-
autoCrud: {
|
|
245
|
-
resources: {
|
|
246
|
-
// Guest View: Only these columns are visible to unauthenticated users.
|
|
247
|
-
// Access control (who can list/read) is managed by your DB permissions.
|
|
248
|
-
users: ['id', 'name', 'avatar'],
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
## 👤 Owner-based Permissions (RBAC)
|
|
256
|
-
|
|
257
|
-
In addition to standard `create`, `read`, `update`, and `delete` permissions, you can assign **Ownership Permissions**:
|
|
258
|
-
|
|
259
|
-
- `list`: Allows a user to view a list of active records (status='active').
|
|
260
|
-
- `list_all`: Allows a user to view **all** records, including inactive ones (e.g., status='inactive', 'draft').
|
|
261
|
-
- `update_own`: Allows a user to update a record **only if they created it**.
|
|
262
|
-
- `delete_own`: Allows a user to delete a record **only if they created it**.
|
|
263
|
-
|
|
264
|
-
**How it works:**
|
|
265
|
-
The module checks for ownership using the following logic:
|
|
266
|
-
1. **Standard Tables:** Checks if the record has a `createdBy` (or `userId`) column that matches the logged-in user's ID.
|
|
267
|
-
2. **Users Table:** Checks if the record being accessed is the user's own profile (`id` matches).
|
|
268
|
-
|
|
269
|
-
**Prerequisites:**
|
|
270
|
-
Ensure your schema includes a `createdBy` field for resources where you want this behavior:
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
export const posts = sqliteTable('posts', {
|
|
274
|
-
// ...
|
|
275
|
-
createdBy: integer('created_by'), // Recommended
|
|
276
|
-
})
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
## 🎮 Try the Playground
|
|
280
|
-
|
|
281
|
-
Want to see it in action? Clone this repo and try the playground:
|
|
282
|
-
|
|
283
|
-
```bash
|
|
284
|
-
# Clone the repository
|
|
285
|
-
git clone https://github.com/clifordpereira/nuxt-auto-crud.git
|
|
286
|
-
cd nuxt-auto-crud
|
|
287
|
-
|
|
288
|
-
# Install dependencies (parent folder)
|
|
289
|
-
bun install
|
|
290
|
-
|
|
291
|
-
# Run the playground (Fullstack)
|
|
292
|
-
cd playground
|
|
293
|
-
bun install
|
|
294
|
-
bun db:generate
|
|
295
|
-
bun run dev
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
## 📖 Usage Examples
|
|
299
|
-
|
|
300
|
-
### Create a Record
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
const user = await $fetch("/api/users", {
|
|
304
|
-
method: "POST",
|
|
305
|
-
body: {
|
|
306
|
-
name: "Cliford Pereira",
|
|
307
|
-
email: "clifordpereira@gmail.com",
|
|
308
|
-
bio: "Full-Stack Developer",
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### Get All Records
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
const users = await $fetch("/api/users");
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Get Record by ID
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
const user = await $fetch("/api/users/1");
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### Update a Record
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
const updated = await $fetch("/api/users/1", {
|
|
329
|
-
method: "PATCH",
|
|
330
|
-
body: {
|
|
331
|
-
bio: "Updated bio",
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### Delete a Record
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
await $fetch("/api/users/1", {
|
|
340
|
-
method: "DELETE",
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
> **Note:** If authentication is enabled (default):
|
|
345
|
-
> - **Fullstack App:** The module integrates with `nuxt-auth-utils`, so session cookies are handled automatically.
|
|
346
|
-
> - **Backend-only App:** You must include the `Authorization: Bearer <token>` header in your requests.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
## 🔧 Requirements
|
|
351
|
-
|
|
352
|
-
- Nuxt 3 or 4
|
|
353
|
-
- Drizzle ORM (SQLite)
|
|
354
|
-
- NuxtHub >= 0.10.0
|
|
355
|
-
|
|
356
|
-
## 🔗 Links
|
|
357
|
-
|
|
358
|
-
- **📚 Documentation:** [auto-crud.clifland.in](https://auto-crud.clifland.in/docs/auto-crud)
|
|
359
|
-
- **🎬 Video Tutorials:** [YouTube Channel](https://www.youtube.com/@ClifordPereira)
|
|
360
|
-
- **💬 Community:** [Discord](https://discord.gg/FBkQQfRFJM) • [GitHub Discussions](https://github.com/clifordpereira/nuxt-auto-crud/discussions/1)
|
|
361
|
-
- **📦 npm:** [nuxt-auto-crud](https://www.npmjs.com/package/nuxt-auto-crud)
|
|
362
|
-
|
|
363
|
-
## 🤝 Contributing
|
|
364
|
-
|
|
365
|
-
Contributions are welcome! Please check out the [contribution guide](/CONTRIBUTING.md).
|
|
366
|
-
|
|
367
|
-
## 📝 License
|
|
368
|
-
|
|
369
|
-
[MIT License](./LICENSE)
|
|
370
|
-
|
|
371
|
-
## 👨💻 Author
|
|
372
|
-
|
|
373
|
-
Made with ❤️ by [Cliford Pereira](https://github.com/clifordpereira)
|
|
43
|
+
[Creator: Clifland](https://www.clifland.in/)
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -67,6 +67,11 @@ const module$1 = defineNuxtModule({
|
|
|
67
67
|
method: "get",
|
|
68
68
|
handler: resolver.resolve(apiDir, "_relations.get")
|
|
69
69
|
});
|
|
70
|
+
addServerHandler({
|
|
71
|
+
route: "/api/_meta",
|
|
72
|
+
method: "get",
|
|
73
|
+
handler: resolver.resolve(apiDir, "_meta.get")
|
|
74
|
+
});
|
|
70
75
|
addServerHandler({
|
|
71
76
|
route: "/api/:model",
|
|
72
77
|
method: "get",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
2
2
|
import { getUserSession } from "#imports";
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
|
-
import { getTableForModel,
|
|
4
|
+
import { getTableForModel, getZodSchema } from "../../utils/modelMapper.js";
|
|
5
5
|
import { db } from "hub:db";
|
|
6
6
|
import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
|
|
7
7
|
import { RecordNotFoundError } from "../../exceptions.js";
|
|
@@ -10,7 +10,8 @@ export default eventHandler(async (event) => {
|
|
|
10
10
|
const isAdmin = await ensureResourceAccess(event, model, "update", { id });
|
|
11
11
|
const table = getTableForModel(model);
|
|
12
12
|
const body = await readBody(event);
|
|
13
|
-
const
|
|
13
|
+
const schema = getZodSchema(model, "patch");
|
|
14
|
+
const payload = await schema.parseAsync(body);
|
|
14
15
|
if ("status" in payload) {
|
|
15
16
|
const { checkAdminAccess } = await import("../../utils/auth.js");
|
|
16
17
|
const hasStatusPermission = await checkAdminAccess(event, model, "update_status", { id });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
2
2
|
import { getUserSession } from "#imports";
|
|
3
|
-
import { getTableForModel,
|
|
3
|
+
import { getTableForModel, getZodSchema } from "../../utils/modelMapper.js";
|
|
4
4
|
import { db } from "hub:db";
|
|
5
5
|
import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
|
|
6
6
|
export default eventHandler(async (event) => {
|
|
@@ -8,7 +8,8 @@ export default eventHandler(async (event) => {
|
|
|
8
8
|
const isAdmin = await ensureResourceAccess(event, model, "create");
|
|
9
9
|
const table = getTableForModel(model);
|
|
10
10
|
const body = await readBody(event);
|
|
11
|
-
const
|
|
11
|
+
const schema = getZodSchema(model, "insert");
|
|
12
|
+
const payload = await schema.parseAsync(body);
|
|
12
13
|
if ("status" in payload) {
|
|
13
14
|
const { checkAdminAccess } = await import("../../utils/auth.js");
|
|
14
15
|
const hasStatusPermission = await checkAdminAccess(event, model, "update_status");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | {
|
|
2
|
+
architecture: string;
|
|
3
|
+
version: string;
|
|
4
|
+
resources: ({
|
|
5
|
+
resource: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
labelField: string;
|
|
8
|
+
methods: string[];
|
|
9
|
+
fields: {
|
|
10
|
+
name: string;
|
|
11
|
+
type: any;
|
|
12
|
+
required: boolean;
|
|
13
|
+
isEnum: boolean;
|
|
14
|
+
options: any;
|
|
15
|
+
references: any;
|
|
16
|
+
isRelation: boolean;
|
|
17
|
+
isReadOnly: boolean;
|
|
18
|
+
}[];
|
|
19
|
+
} | null)[];
|
|
20
|
+
}>>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { eventHandler, getQuery, getHeader } from "h3";
|
|
2
|
+
import { getTableForModel, getAvailableModels } from "../utils/modelMapper.js";
|
|
3
|
+
import { getTableColumns as getDrizzleTableColumns } from "drizzle-orm";
|
|
4
|
+
import { getTableConfig } from "drizzle-orm/sqlite-core";
|
|
5
|
+
import { PROTECTED_FIELDS, HIDDEN_FIELDS } from "../utils/constants.js";
|
|
6
|
+
import { db } from "hub:db";
|
|
7
|
+
import { ensureAuthenticated } from "../utils/auth.js";
|
|
8
|
+
export default eventHandler(async (event) => {
|
|
9
|
+
await ensureAuthenticated(event);
|
|
10
|
+
const query = getQuery(event);
|
|
11
|
+
const acceptHeader = getHeader(event, "accept") || "";
|
|
12
|
+
const models = getAvailableModels().length > 0 ? getAvailableModels() : Object.keys(db?.query || {});
|
|
13
|
+
const resources = models.map((model) => {
|
|
14
|
+
try {
|
|
15
|
+
const table = getTableForModel(model);
|
|
16
|
+
const columns = getDrizzleTableColumns(table);
|
|
17
|
+
const config = getTableConfig(table);
|
|
18
|
+
const fields = Object.entries(columns).filter(([name]) => !HIDDEN_FIELDS.includes(name)).map(([name, col]) => {
|
|
19
|
+
let references = null;
|
|
20
|
+
const fk = config?.foreignKeys.find((f) => f.reference().columns[0].name === col.name);
|
|
21
|
+
if (fk) {
|
|
22
|
+
references = fk.reference().foreignTable[Symbol.for("drizzle:Name")];
|
|
23
|
+
} else if (col.referenceConfig?.foreignTable) {
|
|
24
|
+
const foreignTable = col.referenceConfig.foreignTable;
|
|
25
|
+
references = foreignTable[Symbol.for("drizzle:Name")] || foreignTable.name;
|
|
26
|
+
}
|
|
27
|
+
const semanticType = col.columnType.toLowerCase().replace("sqlite", "");
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
type: semanticType,
|
|
31
|
+
required: col.notNull || false,
|
|
32
|
+
isEnum: !!col.enumValues,
|
|
33
|
+
options: col.enumValues || null,
|
|
34
|
+
references,
|
|
35
|
+
isRelation: !!references,
|
|
36
|
+
// Agentic Hint: Is this field writable by the user/agent?
|
|
37
|
+
isReadOnly: PROTECTED_FIELDS.includes(name)
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const fieldNames = fields.map((f) => f.name);
|
|
41
|
+
const labelField = fieldNames.find((n) => n === "name") || fieldNames.find((n) => n === "title") || fieldNames.find((n) => n === "email") || "id";
|
|
42
|
+
return {
|
|
43
|
+
resource: model,
|
|
44
|
+
endpoint: `/api/${model}`,
|
|
45
|
+
labelField,
|
|
46
|
+
methods: ["GET", "POST", "PATCH", "DELETE"],
|
|
47
|
+
fields
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}).filter(Boolean);
|
|
53
|
+
const payload = {
|
|
54
|
+
architecture: "Clifland-NAC",
|
|
55
|
+
version: "1.0.0-agentic",
|
|
56
|
+
resources
|
|
57
|
+
};
|
|
58
|
+
const currentToken = getQuery(event).token || getHeader(event, "authorization")?.split(" ")[1];
|
|
59
|
+
const tokenSuffix = currentToken ? `?token=${currentToken}` : "";
|
|
60
|
+
if (query.format === "md" || acceptHeader.includes("text/markdown")) {
|
|
61
|
+
let markdown = `# ${payload.architecture} API Manifest (v${payload.version})
|
|
62
|
+
|
|
63
|
+
`;
|
|
64
|
+
payload.resources.forEach((res) => {
|
|
65
|
+
if (!res) return;
|
|
66
|
+
markdown += `### Resource: ${res.resource}
|
|
67
|
+
`;
|
|
68
|
+
markdown += `- **Endpoint**: \`${res.endpoint}${tokenSuffix}\`
|
|
69
|
+
`;
|
|
70
|
+
markdown += `- **Methods**: ${res.methods.join(", ")}
|
|
71
|
+
`;
|
|
72
|
+
markdown += `- **Primary Label**: \`${res.labelField}\`
|
|
73
|
+
|
|
74
|
+
`;
|
|
75
|
+
markdown += `| Field | Type | Required | Writable | Details |
|
|
76
|
+
`;
|
|
77
|
+
markdown += `| :--- | :--- | :--- | :--- | :--- |
|
|
78
|
+
`;
|
|
79
|
+
res.fields.forEach((f) => {
|
|
80
|
+
const details = f.isEnum && f.options ? `Options: ${f.options.join(", ")}` : f.references ? `Refs: ${f.references}` : "-";
|
|
81
|
+
markdown += `| ${f.name} | ${f.type} | ${f.required ? "\u2705" : "\u274C"} | ${f.isReadOnly ? "\u274C" : "\u2705"} | ${details} |
|
|
82
|
+
`;
|
|
83
|
+
});
|
|
84
|
+
markdown += `
|
|
85
|
+
---
|
|
86
|
+
`;
|
|
87
|
+
});
|
|
88
|
+
return markdown;
|
|
89
|
+
}
|
|
90
|
+
return payload;
|
|
91
|
+
});
|
|
@@ -7,6 +7,13 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
7
7
|
if (!auth?.authentication) {
|
|
8
8
|
return true;
|
|
9
9
|
}
|
|
10
|
+
const authHeader = getHeader(event, "authorization");
|
|
11
|
+
const query = getQuery(event);
|
|
12
|
+
const apiToken = useRuntimeConfig(event).apiSecretToken;
|
|
13
|
+
const token = (authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null) || query.token;
|
|
14
|
+
if (token && apiToken && token === apiToken) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
10
17
|
if (auth.type === "jwt") {
|
|
11
18
|
if (!auth.jwtSecret) {
|
|
12
19
|
console.warn("JWT Secret is not configured but auth type is jwt");
|
|
@@ -40,8 +47,8 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
40
47
|
const hasCreatedBy = "createdBy" in table;
|
|
41
48
|
const hasUserId = "userId" in table;
|
|
42
49
|
if (hasCreatedBy || hasUserId) {
|
|
43
|
-
const
|
|
44
|
-
const record = await
|
|
50
|
+
const query2 = db.select().from(table).where(eq(table.id, Number(context.id)));
|
|
51
|
+
const record = await query2.get();
|
|
45
52
|
if (record) {
|
|
46
53
|
if (hasCreatedBy) {
|
|
47
54
|
if (String(record.createdBy) === String(user.id)) return true;
|
|
@@ -73,12 +80,18 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
73
80
|
}
|
|
74
81
|
export async function ensureAuthenticated(event) {
|
|
75
82
|
const { auth } = useAutoCrudConfig();
|
|
83
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
76
84
|
if (!auth?.authentication) return;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
const authHeader = getHeader(event, "authorization");
|
|
86
|
+
const query = getQuery(event);
|
|
87
|
+
const apiToken = runtimeConfig.apiSecretToken;
|
|
88
|
+
const token = (authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null) || query.token;
|
|
89
|
+
if (token && apiToken && token === apiToken) {
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
92
|
+
if (auth.type === "jwt" && auth.jwtSecret) {
|
|
93
|
+
if (await verifyJwtToken(event, auth.jwtSecret)) return;
|
|
94
|
+
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
95
|
+
}
|
|
83
96
|
await requireUserSession(event);
|
|
84
97
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const PROTECTED_FIELDS = [
|
|
2
|
+
"id",
|
|
3
|
+
"createdAt",
|
|
4
|
+
"updatedAt",
|
|
5
|
+
"deletedAt",
|
|
6
|
+
"createdBy",
|
|
7
|
+
"updatedBy",
|
|
8
|
+
"deletedBy",
|
|
9
|
+
"created_at",
|
|
10
|
+
"updated_at",
|
|
11
|
+
"deleted_at",
|
|
12
|
+
"created_by",
|
|
13
|
+
"updated_by",
|
|
14
|
+
"deleted_by"
|
|
15
|
+
];
|
|
16
|
+
export const HIDDEN_FIELDS = [
|
|
17
|
+
// Sensitive Auth
|
|
18
|
+
"password",
|
|
19
|
+
"resetToken",
|
|
20
|
+
"reset_token",
|
|
21
|
+
"resetExpires",
|
|
22
|
+
"reset_expires",
|
|
23
|
+
"githubId",
|
|
24
|
+
"github_id",
|
|
25
|
+
"googleId",
|
|
26
|
+
"google_id",
|
|
27
|
+
"secret",
|
|
28
|
+
"token",
|
|
29
|
+
// System Fields (Leakage prevention)
|
|
30
|
+
"deletedAt",
|
|
31
|
+
"createdBy",
|
|
32
|
+
"updatedBy",
|
|
33
|
+
"deleted_at",
|
|
34
|
+
"created_by",
|
|
35
|
+
"updated_by"
|
|
36
|
+
];
|