nuxt-auto-crud 1.21.0 โ 1.22.1
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 +15 -141
- package/dist/module.json +1 -1
- package/dist/runtime/server/api/[model]/[id].delete.js +3 -5
- package/dist/runtime/server/api/[model]/[id].get.js +4 -9
- package/dist/runtime/server/api/[model]/[id].patch.js +3 -5
- package/dist/runtime/server/exceptions.d.ts +9 -0
- package/dist/runtime/server/exceptions.js +21 -0
- package/dist/runtime/server/utils/schema.js +10 -6
- package/package.json +6 -4
- package/src/runtime/server/api/[model]/[id].delete.ts +3 -5
- package/src/runtime/server/api/[model]/[id].get.ts +4 -9
- package/src/runtime/server/api/[model]/[id].patch.ts +3 -6
- package/src/runtime/server/exceptions.ts +25 -0
- package/src/runtime/server/utils/schema.ts +16 -12
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nuxt Auto CRUD
|
|
2
2
|
|
|
3
|
-
> **Note:** This module is
|
|
3
|
+
> **Note:** This module is production-ready and actively maintained. The core CRUD API is stable. Minor breaking changes may occasionally occur in advanced features or configuration. We recommend version pinning for production deployments.
|
|
4
4
|
|
|
5
5
|
Auto-expose RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. Minimal configuration required.
|
|
6
6
|
|
|
@@ -56,20 +56,13 @@ If you want to add `nuxt-auto-crud` to an existing project, follow these steps:
|
|
|
56
56
|
#### Install dependencies
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
# Install module and required dependencies
|
|
60
59
|
npm install nuxt-auto-crud @nuxthub/core@^0.10.0 drizzle-orm
|
|
61
|
-
|
|
62
|
-
# Optional: Install auth dependencies if using Session Auth (Recommended)
|
|
63
|
-
npm install nuxt-auth-utils nuxt-authorization
|
|
64
|
-
|
|
60
|
+
npm install nuxt-auth-utils nuxt-authorization # Optional: for authentication
|
|
65
61
|
npm install --save-dev wrangler drizzle-kit
|
|
66
|
-
|
|
67
|
-
# Or using bun
|
|
68
|
-
bun add nuxt-auto-crud @nuxthub/core@latest drizzle-orm
|
|
69
|
-
bun add nuxt-auth-utils nuxt-authorization
|
|
70
|
-
bun add --dev wrangler drizzle-kit
|
|
71
62
|
```
|
|
72
63
|
|
|
64
|
+
> You can also use `bun` or `pnpm` instead of `npm`.
|
|
65
|
+
|
|
73
66
|
#### Configure Nuxt
|
|
74
67
|
|
|
75
68
|
Add the modules to your `nuxt.config.ts`:
|
|
@@ -227,81 +220,18 @@ export default defineConfig({
|
|
|
227
220
|
})
|
|
228
221
|
```
|
|
229
222
|
|
|
230
|
-
## ๐ Authentication
|
|
231
|
-
|
|
232
|
-
The module enables authentication by default. To test APIs without authentication, you can set `auth: false`.
|
|
233
|
-
|
|
234
|
-
### Session Auth (Default)
|
|
235
|
-
|
|
236
|
-
Requires `nuxt-auth-utils` and `nuxt-authorization` to be installed in your project.
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
npm install nuxt-auth-utils nuxt-authorization
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
export default defineNuxtConfig({
|
|
244
|
-
modules: [
|
|
245
|
-
'nuxt-auth-utils',
|
|
246
|
-
'nuxt-authorization',
|
|
247
|
-
'nuxt-auto-crud'
|
|
248
|
-
],
|
|
249
|
-
autoCrud: {
|
|
250
|
-
auth: {
|
|
251
|
-
type: 'session',
|
|
252
|
-
authentication: true, // Enables requireUserSession() check
|
|
253
|
-
authorization: true // Enables authorize(model, action) check
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
})
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### JWT Auth
|
|
223
|
+
## ๐ Authentication
|
|
260
224
|
|
|
261
|
-
|
|
225
|
+
Authentication is enabled by default using **Session Auth** (requires `nuxt-auth-utils` and `nuxt-authorization`).
|
|
262
226
|
|
|
227
|
+
To disable auth for testing:
|
|
263
228
|
```typescript
|
|
264
|
-
|
|
265
|
-
autoCrud: {
|
|
266
|
-
auth: {
|
|
267
|
-
type: 'jwt',
|
|
268
|
-
authentication: true,
|
|
269
|
-
jwtSecret: process.env.JWT_SECRET,
|
|
270
|
-
authorization: true
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
})
|
|
229
|
+
autoCrud: { auth: false }
|
|
274
230
|
```
|
|
275
231
|
|
|
276
|
-
|
|
232
|
+
For **JWT Auth** (backend-only apps) or advanced configuration, see the [Authentication docs](https://auto-crud.clifland.in/docs/configuration/authentication).
|
|
277
233
|
|
|
278
|
-
You can disable authentication entirely for testing or public APIs.
|
|
279
234
|
|
|
280
|
-
```typescript
|
|
281
|
-
export default defineNuxtConfig({
|
|
282
|
-
autoCrud: {
|
|
283
|
-
auth: {
|
|
284
|
-
authentication: false,
|
|
285
|
-
authorization: false
|
|
286
|
-
}
|
|
287
|
-
// OR simply:
|
|
288
|
-
// auth: false
|
|
289
|
-
}
|
|
290
|
-
})
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
## ๐งช Testing
|
|
294
|
-
|
|
295
|
-
This module is tested using **Vitest**.
|
|
296
|
-
|
|
297
|
-
- **Unit Tests:** We cover utility functions and helpers.
|
|
298
|
-
- **E2E Tests:** We verify the API endpoints using a real Nuxt server instance.
|
|
299
|
-
|
|
300
|
-
To run the tests locally:
|
|
301
|
-
|
|
302
|
-
```bash
|
|
303
|
-
npm run test
|
|
304
|
-
```
|
|
305
235
|
|
|
306
236
|
## ๐ก๏ธ Public View Configuration (Field Visibility)
|
|
307
237
|
|
|
@@ -346,12 +276,6 @@ export const posts = sqliteTable('posts', {
|
|
|
346
276
|
})
|
|
347
277
|
```
|
|
348
278
|
|
|
349
|
-
## โ ๏ธ Known Issues
|
|
350
|
-
|
|
351
|
-
- **Automatic Relation Expansion:** The module tries to automatically expand foreign keys (e.g., `user_id` -> `user: { name: ... }`). However, this relies on the foreign key column name matching the target table name (e.g., `user_id` for `users`).
|
|
352
|
-
- **Limitation:** If you have custom FK names like `customer_id` or `author_id` pointing to `users`, the automatic expansion will not work yet.
|
|
353
|
-
- **Workaround:** Ensure your FK columns follow the `tablename_id` convention where possible for now.
|
|
354
|
-
|
|
355
279
|
## ๐ฎ Try the Playground
|
|
356
280
|
|
|
357
281
|
Want to see it in action? Clone this repo and try the playground:
|
|
@@ -421,51 +345,7 @@ await $fetch("/api/users/1", {
|
|
|
421
345
|
> - **Fullstack App:** The module integrates with `nuxt-auth-utils`, so session cookies are handled automatically.
|
|
422
346
|
> - **Backend-only App:** You must include the `Authorization: Bearer <token>` header in your requests.
|
|
423
347
|
|
|
424
|
-
## Configuration
|
|
425
|
-
|
|
426
|
-
### Module Options
|
|
427
|
-
|
|
428
|
-
```typescript
|
|
429
348
|
|
|
430
|
-
export default defineNuxtConfig({
|
|
431
|
-
autoCrud: {
|
|
432
|
-
// Path to your database schema file (relative to project root)
|
|
433
|
-
schemaPath: "server/db/schema", // default
|
|
434
|
-
|
|
435
|
-
// Authentication configuration (see "Authentication Configuration" section)
|
|
436
|
-
auth: {
|
|
437
|
-
// ...
|
|
438
|
-
},
|
|
439
|
-
|
|
440
|
-
// Public Guest View Configuration (Field Visibility)
|
|
441
|
-
resources: {
|
|
442
|
-
users: ['id', 'name', 'avatar'],
|
|
443
|
-
},
|
|
444
|
-
},
|
|
445
|
-
});
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
### Protected Fields
|
|
449
|
-
|
|
450
|
-
By default, the following fields are protected from updates:
|
|
451
|
-
|
|
452
|
-
- `id`
|
|
453
|
-
- `createdAt`
|
|
454
|
-
- `created_at`
|
|
455
|
-
- `updatedAt`
|
|
456
|
-
- `updated_at`
|
|
457
|
-
|
|
458
|
-
You can customize updatable fields in your schema by modifying the `modelMapper.ts` utility.
|
|
459
|
-
|
|
460
|
-
### Hidden Fields
|
|
461
|
-
|
|
462
|
-
By default, the following fields are hidden from API responses for security:
|
|
463
|
-
|
|
464
|
-
- `password`
|
|
465
|
-
- `secret`
|
|
466
|
-
- `token`
|
|
467
|
-
|
|
468
|
-
You can customize hidden fields by modifying the `modelMapper.ts` utility.
|
|
469
349
|
|
|
470
350
|
## ๐ง Requirements
|
|
471
351
|
|
|
@@ -473,18 +353,12 @@ You can customize hidden fields by modifying the `modelMapper.ts` utility.
|
|
|
473
353
|
- Drizzle ORM (SQLite)
|
|
474
354
|
- NuxtHub >= 0.10.0
|
|
475
355
|
|
|
476
|
-
## ๐
|
|
477
|
-
|
|
478
|
-
-
|
|
479
|
-
-
|
|
480
|
-
-
|
|
481
|
-
-
|
|
482
|
-
- **YouTube (Add Schemas):** [https://youtu.be/7gW0KW1KtN0](https://youtu.be/7gW0KW1KtN0)
|
|
483
|
-
- **YouTube (Various Permissions):** [https://www.youtube.com/watch?v=Yty3OCYbwOo](https://www.youtube.com/watch?v=Yty3OCYbwOo)
|
|
484
|
-
- **YouTube (Dynamic RBAC):** [https://www.youtube.com/watch?v=W0ju4grRC9M](https://www.youtube.com/watch?v=W0ju4grRC9M)
|
|
485
|
-
- **npm:** [https://www.npmjs.com/package/nuxt-auto-crud](https://www.npmjs.com/package/nuxt-auto-crud)
|
|
486
|
-
- **Github Discussions:** [https://github.com/clifordpereira/nuxt-auto-crud/discussions/1](https://github.com/clifordpereira/nuxt-auto-crud/discussions/1)
|
|
487
|
-
- **Discord:** [https://discord.gg/hGgyEaGu](https://discord.gg/hGgyEaGu)
|
|
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)
|
|
488
362
|
|
|
489
363
|
## ๐ค Contributing
|
|
490
364
|
|
package/dist/module.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams
|
|
1
|
+
import { eventHandler, getRouterParams } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
3
|
import { getTableForModel, getModelSingularName } from "../../utils/modelMapper.js";
|
|
4
4
|
import { db } from "hub:db";
|
|
5
5
|
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
6
|
+
import { RecordNotFoundError } from "../../exceptions.js";
|
|
6
7
|
export default eventHandler(async (event) => {
|
|
7
8
|
const { model, id } = getRouterParams(event);
|
|
8
9
|
const isAdmin = await ensureResourceAccess(event, model, "delete", { id });
|
|
@@ -10,10 +11,7 @@ export default eventHandler(async (event) => {
|
|
|
10
11
|
const singularName = getModelSingularName(model);
|
|
11
12
|
const deletedRecord = await db.delete(table).where(eq(table.id, Number(id))).returning().get();
|
|
12
13
|
if (!deletedRecord) {
|
|
13
|
-
throw
|
|
14
|
-
statusCode: 404,
|
|
15
|
-
message: `${singularName} not found`
|
|
16
|
-
});
|
|
14
|
+
throw new RecordNotFoundError(`${singularName} not found`);
|
|
17
15
|
}
|
|
18
16
|
return formatResourceResult(model, deletedRecord, isAdmin);
|
|
19
17
|
});
|
|
@@ -1,27 +1,22 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams
|
|
1
|
+
import { eventHandler, getRouterParams } from "h3";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
3
|
import { getTableForModel } from "../../utils/modelMapper.js";
|
|
4
4
|
import { db } from "hub:db";
|
|
5
5
|
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
6
6
|
import { checkAdminAccess } from "../../utils/auth.js";
|
|
7
|
+
import { RecordNotFoundError } from "../../exceptions.js";
|
|
7
8
|
export default eventHandler(async (event) => {
|
|
8
9
|
const { model, id } = getRouterParams(event);
|
|
9
10
|
const isAdmin = await ensureResourceAccess(event, model, "read");
|
|
10
11
|
const table = getTableForModel(model);
|
|
11
12
|
const record = await db.select().from(table).where(eq(table.id, Number(id))).get();
|
|
12
13
|
if (!record) {
|
|
13
|
-
throw
|
|
14
|
-
statusCode: 404,
|
|
15
|
-
message: "Record not found"
|
|
16
|
-
});
|
|
14
|
+
throw new RecordNotFoundError();
|
|
17
15
|
}
|
|
18
16
|
if ("status" in record && record.status !== "active") {
|
|
19
17
|
const canListAll = await checkAdminAccess(event, model, "list_all");
|
|
20
18
|
if (!canListAll) {
|
|
21
|
-
throw
|
|
22
|
-
statusCode: 404,
|
|
23
|
-
message: "Record not found"
|
|
24
|
-
});
|
|
19
|
+
throw new RecordNotFoundError();
|
|
25
20
|
}
|
|
26
21
|
}
|
|
27
22
|
return formatResourceResult(model, record, isAdmin);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { eventHandler, getRouterParams, readBody
|
|
1
|
+
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
2
2
|
import { getUserSession } from "#imports";
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
4
|
import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
|
|
5
5
|
import { db } from "hub:db";
|
|
6
6
|
import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
|
|
7
|
+
import { RecordNotFoundError } from "../../exceptions.js";
|
|
7
8
|
export default eventHandler(async (event) => {
|
|
8
9
|
const { model, id } = getRouterParams(event);
|
|
9
10
|
const isAdmin = await ensureResourceAccess(event, model, "update", { id });
|
|
@@ -25,10 +26,7 @@ export default eventHandler(async (event) => {
|
|
|
25
26
|
}
|
|
26
27
|
const updatedRecord = await db.update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
|
|
27
28
|
if (!updatedRecord) {
|
|
28
|
-
throw
|
|
29
|
-
statusCode: 404,
|
|
30
|
-
message: "Record not found"
|
|
31
|
-
});
|
|
29
|
+
throw new RecordNotFoundError();
|
|
32
30
|
}
|
|
33
31
|
return formatResourceResult(model, updatedRecord, isAdmin);
|
|
34
32
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class AutoCrudError extends Error {
|
|
2
|
+
statusCode: number;
|
|
3
|
+
statusMessage?: string;
|
|
4
|
+
constructor(message: string, statusCode: number);
|
|
5
|
+
toH3Error(): import("h3").H3Error<unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare class RecordNotFoundError extends AutoCrudError {
|
|
8
|
+
constructor(message?: string);
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createError } from "h3";
|
|
2
|
+
export class AutoCrudError extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
statusMessage;
|
|
5
|
+
constructor(message, statusCode) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.statusMessage = message;
|
|
9
|
+
}
|
|
10
|
+
toH3Error() {
|
|
11
|
+
return createError({
|
|
12
|
+
statusCode: this.statusCode,
|
|
13
|
+
message: this.message
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class RecordNotFoundError extends AutoCrudError {
|
|
18
|
+
constructor(message = "Record not found") {
|
|
19
|
+
super(message, 404);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -19,12 +19,13 @@ export function drizzleTableToFields(table, resourceName) {
|
|
|
19
19
|
const config = getTableConfig(table);
|
|
20
20
|
config.foreignKeys.forEach((fk) => {
|
|
21
21
|
const sourceColumnName = fk.reference().columns[0].name;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const propertyName = Object.entries(columns).find(([_, col]) => col.name === sourceColumnName)?.[0];
|
|
23
|
+
if (propertyName) {
|
|
24
|
+
const field = fields.find((f) => f.name === propertyName);
|
|
25
|
+
if (field) {
|
|
26
|
+
const targetTable = fk.reference().foreignTable[Symbol.for("drizzle:Name")];
|
|
27
|
+
field.references = targetTable;
|
|
28
|
+
}
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
31
|
} catch {
|
|
@@ -51,6 +52,9 @@ function mapColumnType(column) {
|
|
|
51
52
|
}
|
|
52
53
|
return { type: "number" };
|
|
53
54
|
}
|
|
55
|
+
if (["content", "description", "bio", "message"].includes(column.name)) {
|
|
56
|
+
return { type: "textarea" };
|
|
57
|
+
}
|
|
54
58
|
return { type: "string" };
|
|
55
59
|
}
|
|
56
60
|
export async function getRelations() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-auto-crud",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.1",
|
|
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",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"prepack": "nuxt-module-build build",
|
|
38
|
-
"dev": "
|
|
39
|
-
"dev:build": "nuxi build playground",
|
|
38
|
+
"dev": "bun run dev:prepare && nuxi dev --bun playground",
|
|
39
|
+
"dev:build": "nuxi build --bun playground",
|
|
40
40
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
41
41
|
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
42
42
|
"lint": "eslint .",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"link": "npm link"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@nuxt/kit": "^4.2.
|
|
50
|
+
"@nuxt/kit": "^4.2.2",
|
|
51
51
|
"@nuxt/scripts": "^0.13.0",
|
|
52
52
|
"@types/pluralize": "^0.0.33",
|
|
53
53
|
"c12": "^2.0.1",
|
|
@@ -78,6 +78,8 @@
|
|
|
78
78
|
"drizzle-orm": "^0.38.3",
|
|
79
79
|
"eslint": "^9.39.1",
|
|
80
80
|
"nuxt": "^4.2.1",
|
|
81
|
+
"nuxt-auth-utils": "^0.5.26",
|
|
82
|
+
"nuxt-authorization": "^0.3.5",
|
|
81
83
|
"typescript": "~5.9.3",
|
|
82
84
|
"vitest": "^4.0.13",
|
|
83
85
|
"vue-tsc": "^3.1.5",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// server/api/[model]/[id].delete.ts
|
|
2
|
-
import { eventHandler, getRouterParams
|
|
2
|
+
import { eventHandler, getRouterParams } from 'h3'
|
|
3
3
|
import { eq } from 'drizzle-orm'
|
|
4
4
|
import { getTableForModel, getModelSingularName } from '../../utils/modelMapper'
|
|
5
5
|
import type { TableWithId } from '../../types'
|
|
6
6
|
// @ts-expect-error - hub:db is a virtual alias
|
|
7
7
|
import { db } from 'hub:db'
|
|
8
8
|
import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
9
|
+
import { RecordNotFoundError } from '../../exceptions'
|
|
9
10
|
|
|
10
11
|
export default eventHandler(async (event) => {
|
|
11
12
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
@@ -21,10 +22,7 @@ export default eventHandler(async (event) => {
|
|
|
21
22
|
.get()
|
|
22
23
|
|
|
23
24
|
if (!deletedRecord) {
|
|
24
|
-
throw
|
|
25
|
-
statusCode: 404,
|
|
26
|
-
message: `${singularName} not found`,
|
|
27
|
-
})
|
|
25
|
+
throw new RecordNotFoundError(`${singularName} not found`)
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
return formatResourceResult(model, deletedRecord as Record<string, unknown>, isAdmin)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// server/api/[model]/[id].get.ts
|
|
2
|
-
import { eventHandler, getRouterParams
|
|
2
|
+
import { eventHandler, getRouterParams } from 'h3'
|
|
3
3
|
import { eq } from 'drizzle-orm'
|
|
4
4
|
import { getTableForModel } from '../../utils/modelMapper'
|
|
5
5
|
import type { TableWithId } from '../../types'
|
|
@@ -7,6 +7,7 @@ import type { TableWithId } from '../../types'
|
|
|
7
7
|
import { db } from 'hub:db'
|
|
8
8
|
import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
9
9
|
import { checkAdminAccess } from '../../utils/auth'
|
|
10
|
+
import { RecordNotFoundError } from '../../exceptions'
|
|
10
11
|
|
|
11
12
|
export default eventHandler(async (event) => {
|
|
12
13
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
@@ -21,10 +22,7 @@ export default eventHandler(async (event) => {
|
|
|
21
22
|
.get()
|
|
22
23
|
|
|
23
24
|
if (!record) {
|
|
24
|
-
throw
|
|
25
|
-
statusCode: 404,
|
|
26
|
-
message: 'Record not found',
|
|
27
|
-
})
|
|
25
|
+
throw new RecordNotFoundError()
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
// Filter inactive rows for non-admins (or those without list_all) if status field exists
|
|
@@ -32,10 +30,7 @@ export default eventHandler(async (event) => {
|
|
|
32
30
|
if ('status' in record && (record as any).status !== 'active') {
|
|
33
31
|
const canListAll = await checkAdminAccess(event, model, 'list_all')
|
|
34
32
|
if (!canListAll) {
|
|
35
|
-
throw
|
|
36
|
-
statusCode: 404,
|
|
37
|
-
message: 'Record not found',
|
|
38
|
-
})
|
|
33
|
+
throw new RecordNotFoundError()
|
|
39
34
|
}
|
|
40
35
|
}
|
|
41
36
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// server/api/[model]/[id].patch.ts
|
|
2
|
-
import { eventHandler, getRouterParams, readBody
|
|
2
|
+
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
3
3
|
import type { H3Event } from 'h3'
|
|
4
4
|
import { getUserSession } from '#imports'
|
|
5
5
|
import { eq } from 'drizzle-orm'
|
|
@@ -9,6 +9,7 @@ import type { TableWithId } from '../../types'
|
|
|
9
9
|
// @ts-expect-error - hub:db is a virtual alias
|
|
10
10
|
import { db } from 'hub:db'
|
|
11
11
|
import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from '../../utils/handler'
|
|
12
|
+
import { RecordNotFoundError } from '../../exceptions'
|
|
12
13
|
|
|
13
14
|
export default eventHandler(async (event) => {
|
|
14
15
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
@@ -20,7 +21,6 @@ export default eventHandler(async (event) => {
|
|
|
20
21
|
const body = await readBody(event)
|
|
21
22
|
const payload = filterUpdatableFields(model, body)
|
|
22
23
|
|
|
23
|
-
// Auto-hash fields based on config (default: ['password'])
|
|
24
24
|
// Auto-hash fields based on config (default: ['password'])
|
|
25
25
|
await hashPayloadFields(payload)
|
|
26
26
|
|
|
@@ -52,10 +52,7 @@ export default eventHandler(async (event) => {
|
|
|
52
52
|
.get()
|
|
53
53
|
|
|
54
54
|
if (!updatedRecord) {
|
|
55
|
-
throw
|
|
56
|
-
statusCode: 404,
|
|
57
|
-
message: 'Record not found',
|
|
58
|
-
})
|
|
55
|
+
throw new RecordNotFoundError()
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
return formatResourceResult(model, updatedRecord as Record<string, unknown>, isAdmin)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createError } from 'h3'
|
|
2
|
+
|
|
3
|
+
export class AutoCrudError extends Error {
|
|
4
|
+
statusCode: number
|
|
5
|
+
statusMessage?: string
|
|
6
|
+
|
|
7
|
+
constructor(message: string, statusCode: number) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.statusCode = statusCode
|
|
10
|
+
this.statusMessage = message
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toH3Error() {
|
|
14
|
+
return createError({
|
|
15
|
+
statusCode: this.statusCode,
|
|
16
|
+
message: this.message,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RecordNotFoundError extends AutoCrudError {
|
|
22
|
+
constructor(message: string = 'Record not found') {
|
|
23
|
+
super(message, 404)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -36,18 +36,18 @@ export function drizzleTableToFields(table: any, resourceName: string) {
|
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
37
|
config.foreignKeys.forEach((fk: any) => {
|
|
38
38
|
const sourceColumnName = fk.reference().columns[0].name
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
|
|
40
|
+
// Find the TS property name (key) that corresponds to this SQL column name
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const propertyName = Object.entries(columns).find(([_, col]: [string, any]) => col.name === sourceColumnName)?.[0]
|
|
43
|
+
|
|
44
|
+
if (propertyName) {
|
|
45
|
+
const field = fields.find(f => f.name === propertyName)
|
|
46
|
+
if (field) {
|
|
47
|
+
// Get target table name
|
|
48
|
+
const targetTable = fk.reference().foreignTable[Symbol.for('drizzle:Name')] as string
|
|
49
|
+
field.references = targetTable
|
|
50
|
+
}
|
|
51
51
|
}
|
|
52
52
|
})
|
|
53
53
|
}
|
|
@@ -86,6 +86,10 @@ function mapColumnType(column: any): { type: string, selectOptions?: string[] }
|
|
|
86
86
|
return { type: 'number' }
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
if (['content', 'description', 'bio', 'message'].includes(column.name)) {
|
|
90
|
+
return { type: 'textarea' }
|
|
91
|
+
}
|
|
92
|
+
|
|
89
93
|
return { type: 'string' }
|
|
90
94
|
}
|
|
91
95
|
|