nuxt-auto-crud 1.19.1 → 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/[model]/[id].delete.js +1 -1
- package/dist/runtime/server/api/[model]/[id].patch.js +10 -0
- package/dist/runtime/server/api/[model]/index.post.js +13 -0
- package/dist/runtime/server/api/_schema/[table].get.d.ts +1 -6
- package/dist/runtime/server/utils/auth.js +12 -4
- 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/api/[model]/[id].delete.ts +1 -1
- package/src/runtime/server/api/[model]/[id].patch.ts +16 -0
- package/src/runtime/server/api/[model]/index.post.ts +23 -0
- package/src/runtime/server/utils/auth.ts +21 -10
- 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
|
};
|
|
@@ -5,7 +5,7 @@ import { db } from "hub:db";
|
|
|
5
5
|
import { ensureResourceAccess, formatResourceResult } from "../../utils/handler.js";
|
|
6
6
|
export default eventHandler(async (event) => {
|
|
7
7
|
const { model, id } = getRouterParams(event);
|
|
8
|
-
const isAdmin = await ensureResourceAccess(event, model, "delete");
|
|
8
|
+
const isAdmin = await ensureResourceAccess(event, model, "delete", { id });
|
|
9
9
|
const table = getTableForModel(model);
|
|
10
10
|
const singularName = getModelSingularName(model);
|
|
11
11
|
const deletedRecord = await db.delete(table).where(eq(table.id, Number(id))).returning().get();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, readBody, createError } from "h3";
|
|
2
|
+
import { getUserSession } from "#imports";
|
|
2
3
|
import { eq } from "drizzle-orm";
|
|
3
4
|
import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
|
|
4
5
|
import { db } from "hub:db";
|
|
@@ -13,6 +14,15 @@ export default eventHandler(async (event) => {
|
|
|
13
14
|
if ("updatedAt" in table) {
|
|
14
15
|
payload.updatedAt = /* @__PURE__ */ new Date();
|
|
15
16
|
}
|
|
17
|
+
try {
|
|
18
|
+
const session = await getUserSession(event);
|
|
19
|
+
if (session?.user?.id) {
|
|
20
|
+
if ("updatedBy" in table) {
|
|
21
|
+
payload.updatedBy = session.user.id;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
16
26
|
const updatedRecord = await db.update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
|
|
17
27
|
if (!updatedRecord) {
|
|
18
28
|
throw createError({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { eventHandler, getRouterParams, readBody } from "h3";
|
|
2
|
+
import { getUserSession } from "#imports";
|
|
2
3
|
import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
|
|
3
4
|
import { db } from "hub:db";
|
|
4
5
|
import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from "../../utils/handler.js";
|
|
@@ -9,6 +10,18 @@ export default eventHandler(async (event) => {
|
|
|
9
10
|
const body = await readBody(event);
|
|
10
11
|
const payload = filterUpdatableFields(model, body);
|
|
11
12
|
await hashPayloadFields(payload);
|
|
13
|
+
try {
|
|
14
|
+
const session = await getUserSession(event);
|
|
15
|
+
if (session?.user?.id) {
|
|
16
|
+
if ("createdBy" in table) {
|
|
17
|
+
payload.createdBy = session.user.id;
|
|
18
|
+
}
|
|
19
|
+
if ("updatedBy" in table) {
|
|
20
|
+
payload.updatedBy = session.user.id;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
12
25
|
const newRecord = await db.insert(table).values(payload).returning().get();
|
|
13
26
|
return formatResourceResult(model, newRecord, isAdmin);
|
|
14
27
|
});
|
|
@@ -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;
|
|
@@ -37,10 +37,18 @@ export async function checkAdminAccess(event, model, action, context) {
|
|
|
37
37
|
if (model === "users" && String(context.id) === String(user.id)) {
|
|
38
38
|
return true;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const hasCreatedBy = "createdBy" in table;
|
|
41
|
+
const hasUserId = "userId" in table;
|
|
42
|
+
if (hasCreatedBy || hasUserId) {
|
|
43
|
+
const query = db.select().from(table).where(eq(table.id, Number(context.id)));
|
|
44
|
+
const record = await query.get();
|
|
45
|
+
if (record) {
|
|
46
|
+
if (hasCreatedBy) {
|
|
47
|
+
if (String(record.createdBy) === String(user.id)) return true;
|
|
48
|
+
}
|
|
49
|
+
if (hasUserId) {
|
|
50
|
+
if (String(record.userId) === String(user.id)) return true;
|
|
51
|
+
}
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
} catch (e) {
|
|
@@ -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
|
}
|
|
@@ -9,7 +9,7 @@ import { ensureResourceAccess, formatResourceResult } from '../../utils/handler'
|
|
|
9
9
|
|
|
10
10
|
export default eventHandler(async (event) => {
|
|
11
11
|
const { model, id } = getRouterParams(event) as { model: string, id: string }
|
|
12
|
-
const isAdmin = await ensureResourceAccess(event, model, 'delete')
|
|
12
|
+
const isAdmin = await ensureResourceAccess(event, model, 'delete', { id })
|
|
13
13
|
|
|
14
14
|
const table = getTableForModel(model) as TableWithId
|
|
15
15
|
const singularName = getModelSingularName(model)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// server/api/[model]/[id].patch.ts
|
|
2
2
|
import { eventHandler, getRouterParams, readBody, createError } from 'h3'
|
|
3
|
+
import type { H3Event } from 'h3'
|
|
4
|
+
import { getUserSession } from '#imports'
|
|
3
5
|
import { eq } from 'drizzle-orm'
|
|
4
6
|
import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
|
|
5
7
|
import type { TableWithId } from '../../types'
|
|
@@ -28,6 +30,20 @@ export default eventHandler(async (event) => {
|
|
|
28
30
|
(payload as any).updatedAt = new Date()
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// Inject updatedBy if user is authenticated
|
|
34
|
+
try {
|
|
35
|
+
const session = await (getUserSession as (event: H3Event) => Promise<{ user: { id: string | number } | null }>)(event)
|
|
36
|
+
if (session?.user?.id) {
|
|
37
|
+
if ('updatedBy' in table) {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
(payload as any).updatedBy = session.user.id
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// No session available
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
const updatedRecord = await db
|
|
32
48
|
.update(table)
|
|
33
49
|
.set(payload)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// server/api/[model]/index.post.ts
|
|
2
2
|
import { eventHandler, getRouterParams, readBody } from 'h3'
|
|
3
|
+
import type { H3Event } from 'h3'
|
|
4
|
+
import { getUserSession } from '#imports'
|
|
3
5
|
import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
|
|
4
6
|
// @ts-expect-error - hub:db is a virtual alias
|
|
5
7
|
import { db } from 'hub:db'
|
|
@@ -17,6 +19,27 @@ export default eventHandler(async (event) => {
|
|
|
17
19
|
// Auto-hash fields based on config (default: ['password'])
|
|
18
20
|
await hashPayloadFields(payload)
|
|
19
21
|
|
|
22
|
+
// Inject createdBy/updatedBy if user is authenticated
|
|
23
|
+
try {
|
|
24
|
+
const session = await (getUserSession as (event: H3Event) => Promise<{ user: { id: string | number } | null }>)(event)
|
|
25
|
+
if (session?.user?.id) {
|
|
26
|
+
// Check if table has columns before assigning (optional but safer if we had strict types)
|
|
27
|
+
// Since we are passing payload to .values(), extra keys might be ignored or cause error depending on driver
|
|
28
|
+
// Using 'in' table check is good practice
|
|
29
|
+
if ('createdBy' in table) {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
(payload as any).createdBy = session.user.id
|
|
32
|
+
}
|
|
33
|
+
if ('updatedBy' in table) {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
(payload as any).updatedBy = session.user.id
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// No session available
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
const newRecord = await db.insert(table).values(payload).returning().get()
|
|
21
44
|
|
|
22
45
|
return formatResourceResult(model, newRecord as Record<string, unknown>, isAdmin)
|
|
@@ -62,16 +62,27 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
|
|
|
62
62
|
return true
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// Standard case: Check 'userId' column for ownership
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
// Standard case: Check 'createdBy' or 'userId' column for ownership
|
|
66
|
+
|
|
67
|
+
// const columns = table.columns || {}
|
|
68
|
+
|
|
69
|
+
const hasCreatedBy = 'createdBy' in table
|
|
70
|
+
const hasUserId = 'userId' in table
|
|
71
|
+
|
|
72
|
+
if (hasCreatedBy || hasUserId) {
|
|
73
|
+
// @ts-expect-error - table is dynamic
|
|
74
|
+
const query = db.select().from(table).where(eq(table.id, Number(context.id)))
|
|
75
|
+
const record = await query.get()
|
|
76
|
+
|
|
77
|
+
if (record) {
|
|
78
|
+
// Check createdBy
|
|
79
|
+
if (hasCreatedBy) {
|
|
80
|
+
if (String(record.createdBy) === String(user.id)) return true
|
|
81
|
+
}
|
|
82
|
+
// Check userId (legacy)
|
|
83
|
+
if (hasUserId) {
|
|
84
|
+
if (String(record.userId) === String(user.id)) return true
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
}
|
|
@@ -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
|