nuxt-auto-crud 1.20.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 +23 -4
- package/dist/module.json +1 -1
- package/dist/runtime/composables/useRelationDisplay.d.ts +1 -0
- package/dist/runtime/composables/useRelationDisplay.js +7 -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/api/_schema/[table].get.d.ts +1 -6
- package/dist/runtime/server/exceptions.d.ts +9 -0
- package/dist/runtime/server/exceptions.js +21 -0
- package/dist/runtime/server/utils/schema.d.ts +9 -12
- package/dist/runtime/server/utils/schema.js +29 -7
- package/package.json +5 -5
- package/src/runtime/composables/useRelationDisplay.ts +8 -1
- 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 +53 -10
package/README.md
CHANGED
|
@@ -321,11 +321,30 @@ export default defineNuxtConfig({
|
|
|
321
321
|
})
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
-
## ⚠️ Known Issues
|
|
325
324
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
## 👤 Owner-based Permissions (RBAC)
|
|
326
|
+
|
|
327
|
+
In addition to standard `create`, `read`, `update`, and `delete` permissions, you can assign **Ownership Permissions**:
|
|
328
|
+
|
|
329
|
+
- `list`: Allows a user to view a list of active records (status='active').
|
|
330
|
+
- `list_all`: Allows a user to view **all** records, including inactive ones (e.g., status='inactive', 'draft').
|
|
331
|
+
- `update_own`: Allows a user to update a record **only if they created it**.
|
|
332
|
+
- `delete_own`: Allows a user to delete a record **only if they created it**.
|
|
333
|
+
|
|
334
|
+
**How it works:**
|
|
335
|
+
The module checks for ownership using the following logic:
|
|
336
|
+
1. **Standard Tables:** Checks if the record has a `createdBy` (or `userId`) column that matches the logged-in user's ID.
|
|
337
|
+
2. **Users Table:** Checks if the record being accessed is the user's own profile (`id` matches).
|
|
338
|
+
|
|
339
|
+
**Prerequisites:**
|
|
340
|
+
Ensure your schema includes a `createdBy` field for resources where you want this behavior:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
export const posts = sqliteTable('posts', {
|
|
344
|
+
// ...
|
|
345
|
+
createdBy: integer('created_by'), // Recommended
|
|
346
|
+
})
|
|
347
|
+
```
|
|
329
348
|
|
|
330
349
|
## 🎮 Try the Playground
|
|
331
350
|
|
package/dist/module.json
CHANGED
|
@@ -9,4 +9,5 @@ export declare const useRelationDisplay: (schema: {
|
|
|
9
9
|
fetchRelations: () => Promise<void>;
|
|
10
10
|
getDisplayValue: (key: string, value: unknown) => unknown;
|
|
11
11
|
relationsMap: import("vue").Ref<Record<string, Record<string, string>>, Record<string, Record<string, string>>>;
|
|
12
|
+
forbiddenRelations: import("vue").Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>;
|
|
12
13
|
};
|
|
@@ -4,6 +4,7 @@ export const useRelationDisplay = (schema) => {
|
|
|
4
4
|
const relationsMap = ref({});
|
|
5
5
|
const displayValues = ref({});
|
|
6
6
|
const headers = useRequestHeaders(["cookie"]);
|
|
7
|
+
const forbiddenRelations = ref(/* @__PURE__ */ new Set());
|
|
7
8
|
const fetchRelations = async () => {
|
|
8
9
|
const { data: relations } = await useFetch("/api/_relations");
|
|
9
10
|
if (relations.value) {
|
|
@@ -30,6 +31,10 @@ export const useRelationDisplay = (schema) => {
|
|
|
30
31
|
}
|
|
31
32
|
} catch (error) {
|
|
32
33
|
console.error(`Failed to fetch relation data for ${targetTable}:`, error);
|
|
34
|
+
const err = error;
|
|
35
|
+
if (err?.statusCode === 403) {
|
|
36
|
+
forbiddenRelations.value.add(fieldName);
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
39
|
})
|
|
35
40
|
);
|
|
@@ -43,6 +48,7 @@ export const useRelationDisplay = (schema) => {
|
|
|
43
48
|
return {
|
|
44
49
|
fetchRelations,
|
|
45
50
|
getDisplayValue,
|
|
46
|
-
relationsMap
|
|
51
|
+
relationsMap,
|
|
52
|
+
forbiddenRelations
|
|
47
53
|
};
|
|
48
54
|
};
|
|
@@ -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
|
});
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
2
2
|
resource: string;
|
|
3
|
-
fields:
|
|
4
|
-
name: string;
|
|
5
|
-
type: string;
|
|
6
|
-
required: any;
|
|
7
|
-
selectOptions: string[] | undefined;
|
|
8
|
-
}[];
|
|
3
|
+
fields: import("../../utils/schema.js").Field[];
|
|
9
4
|
}>>;
|
|
10
5
|
export default _default;
|
|
@@ -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
|
+
}
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
+
export interface Field {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
required: boolean;
|
|
5
|
+
selectOptions?: string[];
|
|
6
|
+
references?: string;
|
|
7
|
+
}
|
|
1
8
|
export declare function drizzleTableToFields(table: any, resourceName: string): {
|
|
2
9
|
resource: string;
|
|
3
|
-
fields:
|
|
4
|
-
name: string;
|
|
5
|
-
type: string;
|
|
6
|
-
required: any;
|
|
7
|
-
selectOptions: string[] | undefined;
|
|
8
|
-
}[];
|
|
10
|
+
fields: Field[];
|
|
9
11
|
};
|
|
10
12
|
export declare function getRelations(): Promise<Record<string, Record<string, string>>>;
|
|
11
13
|
export declare function getAllSchemas(): Promise<Record<string, any>>;
|
|
12
14
|
export declare function getSchema(tableName: string): Promise<{
|
|
13
15
|
resource: string;
|
|
14
|
-
fields:
|
|
15
|
-
name: string;
|
|
16
|
-
type: string;
|
|
17
|
-
required: any;
|
|
18
|
-
selectOptions: string[] | undefined;
|
|
19
|
-
}[];
|
|
16
|
+
fields: Field[];
|
|
20
17
|
} | undefined>;
|
|
@@ -15,6 +15,21 @@ export function drizzleTableToFields(table, resourceName) {
|
|
|
15
15
|
selectOptions
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
try {
|
|
19
|
+
const config = getTableConfig(table);
|
|
20
|
+
config.foreignKeys.forEach((fk) => {
|
|
21
|
+
const sourceColumnName = fk.reference().columns[0].name;
|
|
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
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
18
33
|
return {
|
|
19
34
|
resource: resourceName,
|
|
20
35
|
fields
|
|
@@ -37,6 +52,9 @@ function mapColumnType(column) {
|
|
|
37
52
|
}
|
|
38
53
|
return { type: "number" };
|
|
39
54
|
}
|
|
55
|
+
if (["content", "description", "bio", "message"].includes(column.name)) {
|
|
56
|
+
return { type: "textarea" };
|
|
57
|
+
}
|
|
40
58
|
return { type: "string" };
|
|
41
59
|
}
|
|
42
60
|
export async function getRelations() {
|
|
@@ -44,14 +62,18 @@ export async function getRelations() {
|
|
|
44
62
|
for (const [tableName, table] of Object.entries(modelTableMap)) {
|
|
45
63
|
try {
|
|
46
64
|
const config = getTableConfig(table);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
const tableRelations = {};
|
|
66
|
+
relations[tableName] = tableRelations;
|
|
67
|
+
const columns = getTableColumns(table);
|
|
68
|
+
const columnToProperty = {};
|
|
69
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
70
|
+
const columnName = col.name;
|
|
71
|
+
columnToProperty[columnName] = key;
|
|
72
|
+
if (["createdBy", "created_by", "updatedBy", "updated_by", "deletedBy", "deleted_by"].includes(key)) {
|
|
73
|
+
tableRelations[key] = "users";
|
|
54
74
|
}
|
|
75
|
+
}
|
|
76
|
+
if (config.foreignKeys.length > 0) {
|
|
55
77
|
config.foreignKeys.forEach((fk) => {
|
|
56
78
|
const sourceColumnName = fk.reference().columns[0].name;
|
|
57
79
|
const sourceProperty = columnToProperty[sourceColumnName] || sourceColumnName;
|
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,7 +78,7 @@
|
|
|
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.
|
|
81
|
+
"nuxt-auth-utils": "^0.5.26",
|
|
82
82
|
"nuxt-authorization": "^0.3.5",
|
|
83
83
|
"typescript": "~5.9.3",
|
|
84
84
|
"vitest": "^4.0.13",
|
|
@@ -11,6 +11,8 @@ export const useRelationDisplay = (
|
|
|
11
11
|
const displayValues = ref<Record<string, Record<string, string>>>({})
|
|
12
12
|
const headers = useRequestHeaders(['cookie'])
|
|
13
13
|
|
|
14
|
+
const forbiddenRelations = ref<Set<string>>(new Set())
|
|
15
|
+
|
|
14
16
|
const fetchRelations = async () => {
|
|
15
17
|
// 1. Fetch relations metadata
|
|
16
18
|
const { data: relations } = await useFetch<Record<string, Record<string, string>>>('/api/_relations')
|
|
@@ -45,8 +47,12 @@ export const useRelationDisplay = (
|
|
|
45
47
|
)
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
catch (error) {
|
|
50
|
+
catch (error: unknown) {
|
|
49
51
|
console.error(`Failed to fetch relation data for ${targetTable}:`, error)
|
|
52
|
+
const err = error as { statusCode?: number }
|
|
53
|
+
if (err?.statusCode === 403) {
|
|
54
|
+
forbiddenRelations.value.add(fieldName)
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
}),
|
|
52
58
|
)
|
|
@@ -63,5 +69,6 @@ export const useRelationDisplay = (
|
|
|
63
69
|
fetchRelations,
|
|
64
70
|
getDisplayValue,
|
|
65
71
|
relationsMap,
|
|
72
|
+
forbiddenRelations,
|
|
66
73
|
}
|
|
67
74
|
}
|
|
@@ -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
|
+
}
|
|
@@ -2,10 +2,18 @@ import { getTableColumns } from 'drizzle-orm'
|
|
|
2
2
|
import { getTableConfig } from 'drizzle-orm/sqlite-core'
|
|
3
3
|
import { modelTableMap } from './modelMapper'
|
|
4
4
|
|
|
5
|
+
export interface Field {
|
|
6
|
+
name: string
|
|
7
|
+
type: string
|
|
8
|
+
required: boolean
|
|
9
|
+
selectOptions?: string[]
|
|
10
|
+
references?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
14
|
export function drizzleTableToFields(table: any, resourceName: string) {
|
|
7
15
|
const columns = getTableColumns(table)
|
|
8
|
-
const fields = []
|
|
16
|
+
const fields: Field[] = []
|
|
9
17
|
|
|
10
18
|
for (const [key, col] of Object.entries(columns)) {
|
|
11
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -22,6 +30,31 @@ export function drizzleTableToFields(table: any, resourceName: string) {
|
|
|
22
30
|
})
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
try {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
const config = getTableConfig(table as any)
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
config.foreignKeys.forEach((fk: any) => {
|
|
38
|
+
const sourceColumnName = fk.reference().columns[0].name
|
|
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
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Ignore error if getTableConfig fails (e.g. not a Drizzle table)
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
return {
|
|
26
59
|
resource: resourceName,
|
|
27
60
|
fields,
|
|
@@ -53,6 +86,10 @@ function mapColumnType(column: any): { type: string, selectOptions?: string[] }
|
|
|
53
86
|
return { type: 'number' }
|
|
54
87
|
}
|
|
55
88
|
|
|
89
|
+
if (['content', 'description', 'bio', 'message'].includes(column.name)) {
|
|
90
|
+
return { type: 'textarea' }
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
return { type: 'string' }
|
|
57
94
|
}
|
|
58
95
|
|
|
@@ -63,19 +100,25 @@ export async function getRelations() {
|
|
|
63
100
|
try {
|
|
64
101
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
102
|
const config = getTableConfig(table as any)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
relations[tableName] = tableRelations
|
|
103
|
+
const tableRelations: Record<string, string> = {}
|
|
104
|
+
relations[tableName] = tableRelations
|
|
69
105
|
|
|
70
|
-
|
|
106
|
+
// Map column names to property names
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
const columns = getTableColumns(table as any)
|
|
109
|
+
const columnToProperty: Record<string, string> = {}
|
|
110
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
71
111
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
112
|
+
const columnName = (col as any).name
|
|
113
|
+
columnToProperty[columnName] = key
|
|
114
|
+
|
|
115
|
+
// Auto-link createdBy/updatedBy to users table
|
|
116
|
+
if (['createdBy', 'created_by', 'updatedBy', 'updated_by', 'deletedBy', 'deleted_by'].includes(key)) {
|
|
117
|
+
tableRelations[key] = 'users'
|
|
77
118
|
}
|
|
119
|
+
}
|
|
78
120
|
|
|
121
|
+
if (config.foreignKeys.length > 0) {
|
|
79
122
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
123
|
config.foreignKeys.forEach((fk: any) => {
|
|
81
124
|
const sourceColumnName = fk.reference().columns[0].name
|