nuxt-auto-crud 1.20.0 → 1.21.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 +25 -0
- 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/_schema/[table].get.d.ts +1 -6
- package/dist/runtime/server/utils/schema.d.ts +9 -12
- package/dist/runtime/server/utils/schema.js +25 -7
- package/package.json +1 -3
- package/src/runtime/composables/useRelationDisplay.ts +8 -1
- package/src/runtime/server/utils/schema.ts +49 -10
package/README.md
CHANGED
|
@@ -321,6 +321,31 @@ export default defineNuxtConfig({
|
|
|
321
321
|
})
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
+
|
|
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
|
+
```
|
|
348
|
+
|
|
324
349
|
## ⚠️ Known Issues
|
|
325
350
|
|
|
326
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`).
|
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,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;
|
|
@@ -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,20 @@ 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 field = fields.find((f) => {
|
|
23
|
+
return f.name === sourceColumnName;
|
|
24
|
+
});
|
|
25
|
+
if (field) {
|
|
26
|
+
const targetTable = fk.reference().foreignTable[Symbol.for("drizzle:Name")];
|
|
27
|
+
field.references = targetTable;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
18
32
|
return {
|
|
19
33
|
resource: resourceName,
|
|
20
34
|
fields
|
|
@@ -44,14 +58,18 @@ export async function getRelations() {
|
|
|
44
58
|
for (const [tableName, table] of Object.entries(modelTableMap)) {
|
|
45
59
|
try {
|
|
46
60
|
const config = getTableConfig(table);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
const tableRelations = {};
|
|
62
|
+
relations[tableName] = tableRelations;
|
|
63
|
+
const columns = getTableColumns(table);
|
|
64
|
+
const columnToProperty = {};
|
|
65
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
66
|
+
const columnName = col.name;
|
|
67
|
+
columnToProperty[columnName] = key;
|
|
68
|
+
if (["createdBy", "created_by", "updatedBy", "updated_by", "deletedBy", "deleted_by"].includes(key)) {
|
|
69
|
+
tableRelations[key] = "users";
|
|
54
70
|
}
|
|
71
|
+
}
|
|
72
|
+
if (config.foreignKeys.length > 0) {
|
|
55
73
|
config.foreignKeys.forEach((fk) => {
|
|
56
74
|
const sourceColumnName = fk.reference().columns[0].name;
|
|
57
75
|
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.21.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",
|
|
@@ -78,8 +78,6 @@
|
|
|
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.25",
|
|
82
|
-
"nuxt-authorization": "^0.3.5",
|
|
83
81
|
"typescript": "~5.9.3",
|
|
84
82
|
"vitest": "^4.0.13",
|
|
85
83
|
"vue-tsc": "^3.1.5",
|
|
@@ -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
|
}
|
|
@@ -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
|
+
// Find the field that matches this column
|
|
40
|
+
const field = fields.find((f) => {
|
|
41
|
+
// In simple cases, field.name matches column name.
|
|
42
|
+
// If camelCase mapping handles it differently, we might need adjustments,
|
|
43
|
+
// but typically Drizzle key = field name.
|
|
44
|
+
return f.name === sourceColumnName
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (field) {
|
|
48
|
+
// Get target table name
|
|
49
|
+
const targetTable = fk.reference().foreignTable[Symbol.for('drizzle:Name')] as string
|
|
50
|
+
field.references = targetTable
|
|
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,
|
|
@@ -63,19 +96,25 @@ export async function getRelations() {
|
|
|
63
96
|
try {
|
|
64
97
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
98
|
const config = getTableConfig(table as any)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
relations[tableName] = tableRelations
|
|
99
|
+
const tableRelations: Record<string, string> = {}
|
|
100
|
+
relations[tableName] = tableRelations
|
|
69
101
|
|
|
70
|
-
|
|
102
|
+
// Map column names to property names
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
const columns = getTableColumns(table as any)
|
|
105
|
+
const columnToProperty: Record<string, string> = {}
|
|
106
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
71
107
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
const columnName = (col as any).name
|
|
109
|
+
columnToProperty[columnName] = key
|
|
110
|
+
|
|
111
|
+
// Auto-link createdBy/updatedBy to users table
|
|
112
|
+
if (['createdBy', 'created_by', 'updatedBy', 'updated_by', 'deletedBy', 'deleted_by'].includes(key)) {
|
|
113
|
+
tableRelations[key] = 'users'
|
|
77
114
|
}
|
|
115
|
+
}
|
|
78
116
|
|
|
117
|
+
if (config.foreignKeys.length > 0) {
|
|
79
118
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
119
|
config.foreignKeys.forEach((fk: any) => {
|
|
81
120
|
const sourceColumnName = fk.reference().columns[0].name
|