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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.20.0",
4
+ "version": "1.21.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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
- if (config.foreignKeys.length > 0) {
48
- const tableRelations = {};
49
- relations[tableName] = tableRelations;
50
- const columns = getTableColumns(table);
51
- const columnToProperty = {};
52
- for (const [key, col] of Object.entries(columns)) {
53
- columnToProperty[col.name] = key;
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.20.0",
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
- if (config.foreignKeys.length > 0) {
67
- const tableRelations: Record<string, string> = {}
68
- relations[tableName] = tableRelations
99
+ const tableRelations: Record<string, string> = {}
100
+ relations[tableName] = tableRelations
69
101
 
70
- // Map column names to property names
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 columns = getTableColumns(table as any)
73
- const columnToProperty: Record<string, string> = {}
74
- for (const [key, col] of Object.entries(columns)) {
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- columnToProperty[(col as any).name] = key
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