nuxt-auto-crud 1.24.0 → 1.25.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 +19 -0
- package/dist/runtime/server/api/_meta.get.js +55 -0
- package/dist/runtime/server/api/_schema/[table].get.d.ts +1 -0
- 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 +86 -0
- 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,19 @@
|
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
2
|
+
architecture: string;
|
|
3
|
+
version: string;
|
|
4
|
+
resources: ({
|
|
5
|
+
resource: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
labelField: string;
|
|
8
|
+
fields: {
|
|
9
|
+
name: string;
|
|
10
|
+
type: any;
|
|
11
|
+
required: boolean;
|
|
12
|
+
isEnum: boolean;
|
|
13
|
+
options: any;
|
|
14
|
+
references: any;
|
|
15
|
+
isRelation: boolean;
|
|
16
|
+
}[];
|
|
17
|
+
} | null)[];
|
|
18
|
+
}>>;
|
|
19
|
+
export default _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { eventHandler } 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 models = getAvailableModels().length > 0 ? getAvailableModels() : Object.keys(db?.query || {});
|
|
11
|
+
const resources = models.map((model) => {
|
|
12
|
+
try {
|
|
13
|
+
const table = getTableForModel(model);
|
|
14
|
+
const columns = getDrizzleTableColumns(table);
|
|
15
|
+
const config = getTableConfig(table);
|
|
16
|
+
const fields = Object.entries(columns).filter(([name]) => !PROTECTED_FIELDS.includes(name) && !HIDDEN_FIELDS.includes(name)).map(([name, col]) => {
|
|
17
|
+
let references = null;
|
|
18
|
+
const fk = config?.foreignKeys.find(
|
|
19
|
+
(f) => f.reference().columns[0].name === col.name
|
|
20
|
+
);
|
|
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
|
+
};
|
|
37
|
+
});
|
|
38
|
+
const fieldNames = fields.map((f) => f.name);
|
|
39
|
+
const labelField = fieldNames.find((n) => n === "name") || fieldNames.find((n) => n === "title") || fieldNames.find((n) => n === "email") || "id";
|
|
40
|
+
return {
|
|
41
|
+
resource: model,
|
|
42
|
+
endpoint: `/api/${model}`,
|
|
43
|
+
labelField,
|
|
44
|
+
fields
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}).filter(Boolean);
|
|
50
|
+
return {
|
|
51
|
+
architecture: "Clifland-NAC",
|
|
52
|
+
version: "1.0.0-agentic",
|
|
53
|
+
resources
|
|
54
|
+
};
|
|
55
|
+
});
|
|
@@ -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
|
+
];
|
|
@@ -1,86 +1,28 @@
|
|
|
1
1
|
import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
-
|
|
3
|
-
* Custom updatable fields configuration (optional)
|
|
4
|
-
* Only define here if you want to override the auto-detection
|
|
5
|
-
*
|
|
6
|
-
* Example:
|
|
7
|
-
* export const customUpdatableFields: Record<string, string[]> = {
|
|
8
|
-
* users: ['name', 'avatar'], // Only these fields can be updated
|
|
9
|
-
* }
|
|
10
|
-
*/
|
|
2
|
+
import type { z } from 'zod';
|
|
11
3
|
export declare const customUpdatableFields: Record<string, string[]>;
|
|
12
|
-
/**
|
|
13
|
-
* Custom hidden fields configuration (optional)
|
|
14
|
-
* Only define here if you want to override the default hidden fields
|
|
15
|
-
*/
|
|
16
4
|
export declare const customHiddenFields: Record<string, string[]>;
|
|
17
|
-
/**
|
|
18
|
-
* Auto-generated model table map
|
|
19
|
-
* Automatically includes all tables from schema
|
|
20
|
-
*/
|
|
21
5
|
export declare const modelTableMap: Record<string, unknown>;
|
|
22
6
|
/**
|
|
23
|
-
*
|
|
24
|
-
* @param modelName - The name of the model (e.g., 'users', 'products')
|
|
25
|
-
* @returns The corresponding database table
|
|
26
|
-
* @throws Error if model is not found
|
|
7
|
+
* @throws 404 if modelName is not found in tableMap.
|
|
27
8
|
*/
|
|
28
9
|
export declare function getTableForModel(modelName: string): SQLiteTable;
|
|
29
|
-
/**
|
|
30
|
-
* Gets the updatable fields for a model
|
|
31
|
-
* @param modelName - The name of the model
|
|
32
|
-
* @returns Array of field names that can be updated
|
|
33
|
-
*/
|
|
34
10
|
export declare function getUpdatableFields(modelName: string): string[];
|
|
35
11
|
/**
|
|
36
|
-
* Filters
|
|
37
|
-
* @param modelName - The name of the model
|
|
38
|
-
* @param data - The data object to filter
|
|
39
|
-
* @returns Filtered object with only updatable fields
|
|
12
|
+
* Filters and coerces data for updates, handling timestamp conversion.
|
|
40
13
|
*/
|
|
41
14
|
export declare function filterUpdatableFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
42
|
-
/**
|
|
43
|
-
* Gets the singular name for a model (for error messages)
|
|
44
|
-
* Uses pluralize library for accurate singular/plural conversion
|
|
45
|
-
* @param modelName - The plural model name
|
|
46
|
-
* @returns The singular name in PascalCase
|
|
47
|
-
*/
|
|
48
15
|
export declare function getModelSingularName(modelName: string): string;
|
|
49
|
-
/**
|
|
50
|
-
* Gets the plural name for a model
|
|
51
|
-
* @param modelName - The model name (singular or plural)
|
|
52
|
-
* @returns The plural name
|
|
53
|
-
* @return The plural name
|
|
54
|
-
*/
|
|
55
16
|
export declare function getModelPluralName(modelName: string): string;
|
|
56
|
-
/**
|
|
57
|
-
* Lists all available models
|
|
58
|
-
* @returns Array of model names
|
|
59
|
-
*/
|
|
60
17
|
export declare function getAvailableModels(): string[];
|
|
61
|
-
/**
|
|
62
|
-
* Gets the hidden fields for a model
|
|
63
|
-
* @param modelName - The name of the model
|
|
64
|
-
* @returns Array of field names that should be hidden
|
|
65
|
-
*/
|
|
66
18
|
export declare function getHiddenFields(modelName: string): string[];
|
|
67
|
-
/**
|
|
68
|
-
* Gets the public columns for a model
|
|
69
|
-
* @param modelName - The name of the model
|
|
70
|
-
* @returns Array of field names that are public (or undefined if all are public)
|
|
71
|
-
*/
|
|
72
19
|
export declare function getPublicColumns(modelName: string): string[] | undefined;
|
|
73
20
|
/**
|
|
74
|
-
*
|
|
75
|
-
* @param modelName - The name of the model
|
|
76
|
-
* @param data - The data object to filter
|
|
77
|
-
* @returns Filtered object
|
|
21
|
+
* Restricts payload to runtimeConfig resource whitelist and filters hidden fields.
|
|
78
22
|
*/
|
|
79
23
|
export declare function filterPublicColumns(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
+
export declare function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
80
25
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @param modelName - The name of the model
|
|
83
|
-
* @param data - The data object to filter
|
|
84
|
-
* @returns Filtered object without hidden fields
|
|
26
|
+
* Generates Zod schema via drizzle-zod, omitting server-managed and protected fields.
|
|
85
27
|
*/
|
|
86
|
-
export declare function
|
|
28
|
+
export declare function getZodSchema(modelName: string, type?: 'insert' | 'patch'): z.ZodObject<any, any>;
|
|
@@ -4,15 +4,10 @@ import { pascalCase } from "scule";
|
|
|
4
4
|
import { getTableColumns as getDrizzleTableColumns, getTableName } from "drizzle-orm";
|
|
5
5
|
import { createError } from "h3";
|
|
6
6
|
import { useRuntimeConfig } from "#imports";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const customUpdatableFields = {
|
|
10
|
-
|
|
11
|
-
// By default, all fields except PROTECTED_FIELDS are updatable
|
|
12
|
-
};
|
|
13
|
-
export const customHiddenFields = {
|
|
14
|
-
// Add custom hidden fields here if needed
|
|
15
|
-
};
|
|
7
|
+
import { createInsertSchema } from "drizzle-zod";
|
|
8
|
+
import { PROTECTED_FIELDS, HIDDEN_FIELDS } from "./constants.js";
|
|
9
|
+
export const customUpdatableFields = {};
|
|
10
|
+
export const customHiddenFields = {};
|
|
16
11
|
function buildModelTableMap() {
|
|
17
12
|
const tableMap = {};
|
|
18
13
|
for (const [key, value] of Object.entries(schema)) {
|
|
@@ -56,7 +51,7 @@ export function getUpdatableFields(modelName) {
|
|
|
56
51
|
const table = modelTableMap[modelName];
|
|
57
52
|
if (!table) return [];
|
|
58
53
|
const allColumns = getTableColumns(table);
|
|
59
|
-
return allColumns.filter((col) => !PROTECTED_FIELDS.includes(col));
|
|
54
|
+
return allColumns.filter((col) => !PROTECTED_FIELDS.includes(col) && !HIDDEN_FIELDS.includes(col));
|
|
60
55
|
}
|
|
61
56
|
export function filterUpdatableFields(modelName, data) {
|
|
62
57
|
const allowedFields = getUpdatableFields(modelName);
|
|
@@ -86,10 +81,7 @@ export function getAvailableModels() {
|
|
|
86
81
|
return Object.keys(modelTableMap);
|
|
87
82
|
}
|
|
88
83
|
export function getHiddenFields(modelName) {
|
|
89
|
-
|
|
90
|
-
return customHiddenFields[modelName];
|
|
91
|
-
}
|
|
92
|
-
return HIDDEN_FIELDS;
|
|
84
|
+
return customHiddenFields[modelName] ?? HIDDEN_FIELDS;
|
|
93
85
|
}
|
|
94
86
|
export function getPublicColumns(modelName) {
|
|
95
87
|
const { resources } = useRuntimeConfig().autoCrud;
|
|
@@ -101,8 +93,9 @@ export function filterPublicColumns(modelName, data) {
|
|
|
101
93
|
return filterHiddenFields(modelName, data);
|
|
102
94
|
}
|
|
103
95
|
const filtered = {};
|
|
96
|
+
const hidden = getHiddenFields(modelName);
|
|
104
97
|
for (const [key, value] of Object.entries(data)) {
|
|
105
|
-
if (publicColumns.includes(key) && !
|
|
98
|
+
if (publicColumns.includes(key) && !hidden.includes(key)) {
|
|
106
99
|
filtered[key] = value;
|
|
107
100
|
}
|
|
108
101
|
}
|
|
@@ -118,3 +111,22 @@ export function filterHiddenFields(modelName, data) {
|
|
|
118
111
|
}
|
|
119
112
|
return filtered;
|
|
120
113
|
}
|
|
114
|
+
export function getZodSchema(modelName, type = "insert") {
|
|
115
|
+
const table = getTableForModel(modelName);
|
|
116
|
+
const schema2 = createInsertSchema(table);
|
|
117
|
+
if (type === "patch") {
|
|
118
|
+
return schema2.partial();
|
|
119
|
+
}
|
|
120
|
+
const OMIT_ON_CREATE = [
|
|
121
|
+
...PROTECTED_FIELDS,
|
|
122
|
+
...HIDDEN_FIELDS
|
|
123
|
+
];
|
|
124
|
+
const columns = getDrizzleTableColumns(table);
|
|
125
|
+
const fieldsToOmit = {};
|
|
126
|
+
OMIT_ON_CREATE.forEach((field) => {
|
|
127
|
+
if (columns[field]) {
|
|
128
|
+
fieldsToOmit[field] = true;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return schema2.omit(fieldsToOmit);
|
|
132
|
+
}
|
|
@@ -7,11 +7,13 @@ export interface Field {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function drizzleTableToFields(table: any, resourceName: string): {
|
|
9
9
|
resource: string;
|
|
10
|
+
labelField: string;
|
|
10
11
|
fields: Field[];
|
|
11
12
|
};
|
|
12
13
|
export declare function getRelations(): Promise<Record<string, Record<string, string>>>;
|
|
13
14
|
export declare function getAllSchemas(): Promise<Record<string, any>>;
|
|
14
15
|
export declare function getSchema(tableName: string): Promise<{
|
|
15
16
|
resource: string;
|
|
17
|
+
labelField: string;
|
|
16
18
|
fields: Field[];
|
|
17
19
|
} | undefined>;
|
|
@@ -15,6 +15,8 @@ export function drizzleTableToFields(table, resourceName) {
|
|
|
15
15
|
selectOptions
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
const fieldNames = fields.map((f) => f.name);
|
|
19
|
+
const labelField = fieldNames.find((n) => n === "name") || fieldNames.find((n) => n === "title") || fieldNames.find((n) => n === "email") || "id";
|
|
18
20
|
try {
|
|
19
21
|
const config = getTableConfig(table);
|
|
20
22
|
config.foreignKeys.forEach((fk) => {
|
|
@@ -32,6 +34,8 @@ export function drizzleTableToFields(table, resourceName) {
|
|
|
32
34
|
}
|
|
33
35
|
return {
|
|
34
36
|
resource: resourceName,
|
|
37
|
+
labelField,
|
|
38
|
+
// metadata point
|
|
35
39
|
fields
|
|
36
40
|
};
|
|
37
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-auto-crud",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Exposes RESTful CRUD APIs for your Nuxt app based solely on your database migrations.",
|
|
5
5
|
"author": "Cliford Pereira",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@nuxt/scripts": "^0.13.0",
|
|
52
52
|
"@types/pluralize": "^0.0.33",
|
|
53
53
|
"c12": "^2.0.1",
|
|
54
|
+
"drizzle-zod": "^0.8.3",
|
|
54
55
|
"jose": "^5.9.6",
|
|
55
56
|
"pluralize": "^8.0.0",
|
|
56
57
|
"scule": "^1.0.0"
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
"drizzle-orm": "^0.38.3",
|
|
79
80
|
"eslint": "^9.39.1",
|
|
80
81
|
"nuxt": "^4.2.1",
|
|
81
|
-
"nuxt-auth-utils": "^0.5.
|
|
82
|
+
"nuxt-auth-utils": "^0.5.27",
|
|
82
83
|
"nuxt-authorization": "^0.3.5",
|
|
83
84
|
"typescript": "~5.9.3",
|
|
84
85
|
"vitest": "^4.0.13",
|
|
@@ -4,7 +4,7 @@ import type { H3Event } from 'h3'
|
|
|
4
4
|
// @ts-expect-error - #imports is a virtual alias
|
|
5
5
|
import { getUserSession } from '#imports'
|
|
6
6
|
import { eq } from 'drizzle-orm'
|
|
7
|
-
import { getTableForModel,
|
|
7
|
+
import { getTableForModel, getZodSchema } from '../../utils/modelMapper'
|
|
8
8
|
import type { TableWithId } from '../../types'
|
|
9
9
|
|
|
10
10
|
// @ts-expect-error - hub:db is a virtual alias
|
|
@@ -20,7 +20,8 @@ export default eventHandler(async (event) => {
|
|
|
20
20
|
const table = getTableForModel(model) as TableWithId
|
|
21
21
|
|
|
22
22
|
const body = await readBody(event)
|
|
23
|
-
const
|
|
23
|
+
const schema = getZodSchema(model, 'patch')
|
|
24
|
+
const payload = await schema.parseAsync(body)
|
|
24
25
|
|
|
25
26
|
// Custom check for status update permission
|
|
26
27
|
if ('status' in payload) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// server/api/[model]/index.post.ts
|
|
2
2
|
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
3
3
|
import type { H3Event } from 'h3'
|
|
4
|
-
// @ts-expect-error - #imports is a virtual alias
|
|
4
|
+
// @ts-expect-error - '#imports' is a virtual alias
|
|
5
5
|
import { getUserSession } from '#imports'
|
|
6
|
-
import { getTableForModel,
|
|
7
|
-
// @ts-expect-error - hub:db is a virtual alias
|
|
6
|
+
import { getTableForModel, getZodSchema } from '../../utils/modelMapper'
|
|
7
|
+
// @ts-expect-error - 'hub:db' is a virtual alias
|
|
8
8
|
import { db } from 'hub:db'
|
|
9
9
|
import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from '../../utils/handler'
|
|
10
10
|
|
|
@@ -15,7 +15,8 @@ export default eventHandler(async (event) => {
|
|
|
15
15
|
const table = getTableForModel(model)
|
|
16
16
|
|
|
17
17
|
const body = await readBody(event)
|
|
18
|
-
const
|
|
18
|
+
const schema = getZodSchema(model, 'insert')
|
|
19
|
+
const payload = await schema.parseAsync(body)
|
|
19
20
|
|
|
20
21
|
// Custom check for status update permission (or just remove it during creation as per requirement)
|
|
21
22
|
if ('status' in payload) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// server/api/_meta.get.ts
|
|
2
|
+
import { eventHandler } from 'h3'
|
|
3
|
+
import { getTableForModel, getAvailableModels } from '../utils/modelMapper'
|
|
4
|
+
import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
|
|
5
|
+
import { getTableConfig } from 'drizzle-orm/sqlite-core'
|
|
6
|
+
import { PROTECTED_FIELDS, HIDDEN_FIELDS } from '../utils/constants'
|
|
7
|
+
// @ts-expect-error - 'hub:db' is a virtual alias
|
|
8
|
+
import { db } from 'hub:db'
|
|
9
|
+
import { ensureAuthenticated } from '../utils/auth'
|
|
10
|
+
|
|
11
|
+
export default eventHandler(async (event) => {
|
|
12
|
+
await ensureAuthenticated(event)
|
|
13
|
+
|
|
14
|
+
const models = getAvailableModels().length > 0
|
|
15
|
+
? getAvailableModels()
|
|
16
|
+
: Object.keys(db?.query || {})
|
|
17
|
+
|
|
18
|
+
const resources = models.map((model) => {
|
|
19
|
+
try {
|
|
20
|
+
const table = getTableForModel(model)
|
|
21
|
+
const columns = getDrizzleTableColumns(table)
|
|
22
|
+
const config = getTableConfig(table)
|
|
23
|
+
|
|
24
|
+
// Map columns to fields
|
|
25
|
+
const fields = Object.entries(columns)
|
|
26
|
+
.filter(([name]) => !PROTECTED_FIELDS.includes(name) && !HIDDEN_FIELDS.includes(name))
|
|
27
|
+
.map(([name, col]) => {
|
|
28
|
+
let references = null
|
|
29
|
+
|
|
30
|
+
// 1. Check for Foreign Keys via getTableConfig (Robust Drizzle Reflection)
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
const fk = config?.foreignKeys.find((f: any) =>
|
|
33
|
+
f.reference().columns[0].name === col.name,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if (fk) {
|
|
37
|
+
// @ts-expect-error - Drizzle internals
|
|
38
|
+
references = fk.reference().foreignTable[Symbol.for('drizzle:Name')]
|
|
39
|
+
}
|
|
40
|
+
// 2. Fallback to inline reference config if symbol lookup fails
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
else if ((col as any).referenceConfig?.foreignTable) {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
const foreignTable = (col as any).referenceConfig.foreignTable
|
|
45
|
+
references = foreignTable[Symbol.for('drizzle:Name')] || foreignTable.name
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Semantic Normalization
|
|
49
|
+
const semanticType = col.columnType.toLowerCase().replace('sqlite', '')
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name,
|
|
53
|
+
type: semanticType,
|
|
54
|
+
required: col.notNull || false,
|
|
55
|
+
isEnum: !!col.enumValues,
|
|
56
|
+
options: col.enumValues || null,
|
|
57
|
+
references,
|
|
58
|
+
isRelation: !!references,
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// 3. Implement Clifland Label Heuristic (name > title > email > id)
|
|
63
|
+
const fieldNames = fields.map(f => f.name)
|
|
64
|
+
const labelField = fieldNames.find(n => n === 'name')
|
|
65
|
+
|| fieldNames.find(n => n === 'title')
|
|
66
|
+
|| fieldNames.find(n => n === 'email')
|
|
67
|
+
|| 'id'
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
resource: model,
|
|
71
|
+
endpoint: `/api/${model}`,
|
|
72
|
+
labelField,
|
|
73
|
+
fields,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}).filter(Boolean)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
architecture: 'Clifland-NAC',
|
|
83
|
+
version: '1.0.0-agentic',
|
|
84
|
+
resources,
|
|
85
|
+
}
|
|
86
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const PROTECTED_FIELDS = [
|
|
2
|
+
'id',
|
|
3
|
+
'createdAt', 'updatedAt', 'deletedAt',
|
|
4
|
+
'createdBy', 'updatedBy', 'deletedBy',
|
|
5
|
+
'created_at', 'updated_at', 'deleted_at',
|
|
6
|
+
'created_by', 'updated_by', 'deleted_by',
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
export const HIDDEN_FIELDS = [
|
|
10
|
+
// Sensitive Auth
|
|
11
|
+
'password',
|
|
12
|
+
'resetToken', 'reset_token',
|
|
13
|
+
'resetExpires', 'reset_expires',
|
|
14
|
+
'githubId', 'github_id',
|
|
15
|
+
'googleId', 'google_id',
|
|
16
|
+
'secret',
|
|
17
|
+
'token',
|
|
18
|
+
// System Fields (Leakage prevention)
|
|
19
|
+
'deletedAt',
|
|
20
|
+
'createdBy',
|
|
21
|
+
'updatedBy',
|
|
22
|
+
'deleted_at',
|
|
23
|
+
'created_by',
|
|
24
|
+
'updated_by',
|
|
25
|
+
]
|
|
@@ -7,53 +7,23 @@ import { getTableColumns as getDrizzleTableColumns, getTableName } from 'drizzle
|
|
|
7
7
|
import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
8
8
|
import { createError } from 'h3'
|
|
9
9
|
import { useRuntimeConfig } from '#imports'
|
|
10
|
+
import { createInsertSchema } from 'drizzle-zod'
|
|
11
|
+
import type { z } from 'zod'
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
* Fields that should never be updatable via PATCH requests
|
|
13
|
-
*/
|
|
14
|
-
const PROTECTED_FIELDS = ['id', 'created_at', 'updated_at', 'createdAt', 'updatedAt']
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Fields that should never be returned in API responses
|
|
18
|
-
*/
|
|
19
|
-
const HIDDEN_FIELDS = ['password', 'secret', 'token']
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Custom updatable fields configuration (optional)
|
|
23
|
-
* Only define here if you want to override the auto-detection
|
|
24
|
-
*
|
|
25
|
-
* Example:
|
|
26
|
-
* export const customUpdatableFields: Record<string, string[]> = {
|
|
27
|
-
* users: ['name', 'avatar'], // Only these fields can be updated
|
|
28
|
-
* }
|
|
29
|
-
*/
|
|
30
|
-
export const customUpdatableFields: Record<string, string[]> = {
|
|
31
|
-
// Add custom field restrictions here if needed
|
|
32
|
-
// By default, all fields except PROTECTED_FIELDS are updatable
|
|
33
|
-
}
|
|
13
|
+
import { PROTECTED_FIELDS, HIDDEN_FIELDS } from './constants'
|
|
34
14
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* Only define here if you want to override the default hidden fields
|
|
38
|
-
*/
|
|
39
|
-
export const customHiddenFields: Record<string, string[]> = {
|
|
40
|
-
// Add custom hidden fields here if needed
|
|
41
|
-
}
|
|
15
|
+
export const customUpdatableFields: Record<string, string[]> = {}
|
|
16
|
+
export const customHiddenFields: Record<string, string[]> = {}
|
|
42
17
|
|
|
43
18
|
/**
|
|
44
|
-
*
|
|
45
|
-
* No manual configuration needed!
|
|
19
|
+
* Builds a map of all exported Drizzle tables from the schema.
|
|
46
20
|
*/
|
|
47
21
|
function buildModelTableMap(): Record<string, unknown> {
|
|
48
22
|
const tableMap: Record<string, unknown> = {}
|
|
49
23
|
|
|
50
|
-
// Iterate through all exports from schema
|
|
51
24
|
for (const [key, value] of Object.entries(schema)) {
|
|
52
|
-
// Check if it's a Drizzle table
|
|
53
25
|
if (value && typeof value === 'object') {
|
|
54
26
|
try {
|
|
55
|
-
// getTableName returns the table name for valid tables, and undefined/null for others (like relations)
|
|
56
|
-
// This is a more robust check than checking for properties
|
|
57
27
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
28
|
const tableName = getTableName(value as any)
|
|
59
29
|
if (tableName) {
|
|
@@ -61,7 +31,7 @@ function buildModelTableMap(): Record<string, unknown> {
|
|
|
61
31
|
}
|
|
62
32
|
}
|
|
63
33
|
catch {
|
|
64
|
-
//
|
|
34
|
+
// Not a table
|
|
65
35
|
}
|
|
66
36
|
}
|
|
67
37
|
}
|
|
@@ -69,17 +39,10 @@ function buildModelTableMap(): Record<string, unknown> {
|
|
|
69
39
|
return tableMap
|
|
70
40
|
}
|
|
71
41
|
|
|
72
|
-
/**
|
|
73
|
-
* Auto-generated model table map
|
|
74
|
-
* Automatically includes all tables from schema
|
|
75
|
-
*/
|
|
76
42
|
export const modelTableMap = buildModelTableMap()
|
|
77
43
|
|
|
78
44
|
/**
|
|
79
|
-
*
|
|
80
|
-
* @param modelName - The name of the model (e.g., 'users', 'products')
|
|
81
|
-
* @returns The corresponding database table
|
|
82
|
-
* @throws Error if model is not found
|
|
45
|
+
* @throws 404 if modelName is not found in tableMap.
|
|
83
46
|
*/
|
|
84
47
|
export function getTableForModel(modelName: string): SQLiteTable {
|
|
85
48
|
const table = modelTableMap[modelName]
|
|
@@ -95,10 +58,6 @@ export function getTableForModel(modelName: string): SQLiteTable {
|
|
|
95
58
|
return table as SQLiteTable
|
|
96
59
|
}
|
|
97
60
|
|
|
98
|
-
/**
|
|
99
|
-
* Auto-detects updatable fields for a table
|
|
100
|
-
* Returns all fields except protected ones (id, createdAt, etc.)
|
|
101
|
-
*/
|
|
102
61
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
62
|
function getTableColumns(table: any): string[] {
|
|
104
63
|
try {
|
|
@@ -111,33 +70,20 @@ function getTableColumns(table: any): string[] {
|
|
|
111
70
|
}
|
|
112
71
|
}
|
|
113
72
|
|
|
114
|
-
/**
|
|
115
|
-
* Gets the updatable fields for a model
|
|
116
|
-
* @param modelName - The name of the model
|
|
117
|
-
* @returns Array of field names that can be updated
|
|
118
|
-
*/
|
|
119
73
|
export function getUpdatableFields(modelName: string): string[] {
|
|
120
|
-
// Check if custom fields are defined for this model
|
|
121
74
|
if (customUpdatableFields[modelName]) {
|
|
122
75
|
return customUpdatableFields[modelName]
|
|
123
76
|
}
|
|
124
77
|
|
|
125
|
-
// Auto-detect from table schema
|
|
126
78
|
const table = modelTableMap[modelName]
|
|
127
|
-
|
|
128
79
|
if (!table) return []
|
|
129
80
|
|
|
130
81
|
const allColumns = getTableColumns(table)
|
|
131
|
-
|
|
132
|
-
// Filter out protected fields
|
|
133
|
-
return allColumns.filter(col => !PROTECTED_FIELDS.includes(col))
|
|
82
|
+
return allColumns.filter(col => !PROTECTED_FIELDS.includes(col) && !HIDDEN_FIELDS.includes(col))
|
|
134
83
|
}
|
|
135
84
|
|
|
136
85
|
/**
|
|
137
|
-
* Filters
|
|
138
|
-
* @param modelName - The name of the model
|
|
139
|
-
* @param data - The data object to filter
|
|
140
|
-
* @returns Filtered object with only updatable fields
|
|
86
|
+
* Filters and coerces data for updates, handling timestamp conversion.
|
|
141
87
|
*/
|
|
142
88
|
export function filterUpdatableFields(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
|
|
143
89
|
const allowedFields = getUpdatableFields(modelName)
|
|
@@ -151,7 +97,6 @@ export function filterUpdatableFields(modelName: string, data: Record<string, un
|
|
|
151
97
|
let value = data[field]
|
|
152
98
|
const column = columns[field]
|
|
153
99
|
|
|
154
|
-
// Coerce timestamp fields to Date objects if they are strings
|
|
155
100
|
if (column && column.mode === 'timestamp' && typeof value === 'string') {
|
|
156
101
|
value = new Date(value)
|
|
157
102
|
}
|
|
@@ -163,79 +108,43 @@ export function filterUpdatableFields(modelName: string, data: Record<string, un
|
|
|
163
108
|
return filtered
|
|
164
109
|
}
|
|
165
110
|
|
|
166
|
-
/**
|
|
167
|
-
* Gets the singular name for a model (for error messages)
|
|
168
|
-
* Uses pluralize library for accurate singular/plural conversion
|
|
169
|
-
* @param modelName - The plural model name
|
|
170
|
-
* @returns The singular name in PascalCase
|
|
171
|
-
*/
|
|
172
111
|
export function getModelSingularName(modelName: string): string {
|
|
173
112
|
const singular = pluralize.singular(modelName)
|
|
174
113
|
return pascalCase(singular)
|
|
175
114
|
}
|
|
176
115
|
|
|
177
|
-
/**
|
|
178
|
-
* Gets the plural name for a model
|
|
179
|
-
* @param modelName - The model name (singular or plural)
|
|
180
|
-
* @returns The plural name
|
|
181
|
-
* @return The plural name
|
|
182
|
-
*/
|
|
183
116
|
export function getModelPluralName(modelName: string): string {
|
|
184
117
|
return pluralize.plural(modelName)
|
|
185
118
|
}
|
|
186
119
|
|
|
187
|
-
/**
|
|
188
|
-
* Lists all available models
|
|
189
|
-
* @returns Array of model names
|
|
190
|
-
*/
|
|
191
120
|
export function getAvailableModels(): string[] {
|
|
192
121
|
return Object.keys(modelTableMap)
|
|
193
122
|
}
|
|
194
123
|
|
|
195
|
-
/**
|
|
196
|
-
* Gets the hidden fields for a model
|
|
197
|
-
* @param modelName - The name of the model
|
|
198
|
-
* @returns Array of field names that should be hidden
|
|
199
|
-
*/
|
|
200
124
|
export function getHiddenFields(modelName: string): string[] {
|
|
201
|
-
|
|
202
|
-
if (customHiddenFields[modelName]) {
|
|
203
|
-
return customHiddenFields[modelName]
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return HIDDEN_FIELDS
|
|
125
|
+
return customHiddenFields[modelName] ?? HIDDEN_FIELDS
|
|
207
126
|
}
|
|
208
127
|
|
|
209
|
-
/**
|
|
210
|
-
* Gets the public columns for a model
|
|
211
|
-
* @param modelName - The name of the model
|
|
212
|
-
* @returns Array of field names that are public (or undefined if all are public)
|
|
213
|
-
*/
|
|
214
128
|
export function getPublicColumns(modelName: string): string[] | undefined {
|
|
215
129
|
const { resources } = useRuntimeConfig().autoCrud
|
|
216
|
-
// Runtime config structure now matches simple key-value
|
|
217
130
|
return resources?.[modelName]
|
|
218
131
|
}
|
|
219
132
|
|
|
220
133
|
/**
|
|
221
|
-
*
|
|
222
|
-
* @param modelName - The name of the model
|
|
223
|
-
* @param data - The data object to filter
|
|
224
|
-
* @returns Filtered object
|
|
134
|
+
* Restricts payload to runtimeConfig resource whitelist and filters hidden fields.
|
|
225
135
|
*/
|
|
226
136
|
export function filterPublicColumns(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
|
|
227
137
|
const publicColumns = getPublicColumns(modelName)
|
|
228
138
|
|
|
229
|
-
// If no public columns configured, return all (except hidden)
|
|
230
139
|
if (!publicColumns) {
|
|
231
140
|
return filterHiddenFields(modelName, data)
|
|
232
141
|
}
|
|
233
142
|
|
|
234
143
|
const filtered: Record<string, unknown> = {}
|
|
144
|
+
const hidden = getHiddenFields(modelName)
|
|
235
145
|
|
|
236
146
|
for (const [key, value] of Object.entries(data)) {
|
|
237
|
-
|
|
238
|
-
if (publicColumns.includes(key) && !getHiddenFields(modelName).includes(key)) {
|
|
147
|
+
if (publicColumns.includes(key) && !hidden.includes(key)) {
|
|
239
148
|
filtered[key] = value
|
|
240
149
|
}
|
|
241
150
|
}
|
|
@@ -243,12 +152,6 @@ export function filterPublicColumns(modelName: string, data: Record<string, unkn
|
|
|
243
152
|
return filtered
|
|
244
153
|
}
|
|
245
154
|
|
|
246
|
-
/**
|
|
247
|
-
* Filters an object to exclude hidden fields
|
|
248
|
-
* @param modelName - The name of the model
|
|
249
|
-
* @param data - The data object to filter
|
|
250
|
-
* @returns Filtered object without hidden fields
|
|
251
|
-
*/
|
|
252
155
|
export function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
|
|
253
156
|
const hiddenFields = getHiddenFields(modelName)
|
|
254
157
|
const filtered: Record<string, unknown> = {}
|
|
@@ -261,3 +164,34 @@ export function filterHiddenFields(modelName: string, data: Record<string, unkno
|
|
|
261
164
|
|
|
262
165
|
return filtered
|
|
263
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generates Zod schema via drizzle-zod, omitting server-managed and protected fields.
|
|
170
|
+
*/
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
export function getZodSchema(modelName: string, type: 'insert' | 'patch' = 'insert'): z.ZodObject<any, any> {
|
|
173
|
+
const table = getTableForModel(modelName)
|
|
174
|
+
const schema = createInsertSchema(table)
|
|
175
|
+
|
|
176
|
+
if (type === 'patch') {
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
+
return schema.partial() as z.ZodObject<any, any>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const OMIT_ON_CREATE = [
|
|
182
|
+
...PROTECTED_FIELDS,
|
|
183
|
+
...HIDDEN_FIELDS,
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
const columns = getDrizzleTableColumns(table)
|
|
187
|
+
const fieldsToOmit: Record<string, true> = {}
|
|
188
|
+
|
|
189
|
+
OMIT_ON_CREATE.forEach((field) => {
|
|
190
|
+
if (columns[field]) {
|
|
191
|
+
fieldsToOmit[field] = true
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
|
+
return (schema as any).omit(fieldsToOmit)
|
|
197
|
+
}
|
|
@@ -30,6 +30,13 @@ export function drizzleTableToFields(table: any, resourceName: string) {
|
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Clifland Heuristic: Auto-detect the primary label for the resource
|
|
34
|
+
const fieldNames = fields.map(f => f.name)
|
|
35
|
+
const labelField = fieldNames.find(n => n === 'name')
|
|
36
|
+
|| fieldNames.find(n => n === 'title')
|
|
37
|
+
|| fieldNames.find(n => n === 'email')
|
|
38
|
+
|| 'id'
|
|
39
|
+
|
|
33
40
|
try {
|
|
34
41
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
42
|
const config = getTableConfig(table as any)
|
|
@@ -57,6 +64,7 @@ export function drizzleTableToFields(table: any, resourceName: string) {
|
|
|
57
64
|
|
|
58
65
|
return {
|
|
59
66
|
resource: resourceName,
|
|
67
|
+
labelField, // metadata point
|
|
60
68
|
fields,
|
|
61
69
|
}
|
|
62
70
|
}
|