nuxt-auto-crud 1.21.0 → 1.22.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 +0 -6
- 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
|
@@ -346,12 +346,6 @@ export const posts = sqliteTable('posts', {
|
|
|
346
346
|
})
|
|
347
347
|
```
|
|
348
348
|
|
|
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
349
|
## 🎮 Try the Playground
|
|
356
350
|
|
|
357
351
|
Want to see it in action? Clone this repo and try the playground:
|
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.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",
|
|
@@ -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
|
|