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 CHANGED
@@ -321,11 +321,30 @@ export default defineNuxtConfig({
321
321
  })
322
322
  ```
323
323
 
324
- ## ⚠️ Known Issues
325
324
 
326
- - **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`).
327
- - **Limitation:** If you have custom FK names like `customer_id` or `author_id` pointing to `users`, the automatic expansion will not work yet.
328
- - **Workaround:** Ensure your FK columns follow the `tablename_id` convention where possible for now.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.20.0",
4
+ "version": "1.22.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,8 +1,9 @@
1
- import { eventHandler, getRouterParams, createError } from "h3";
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 createError({
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, createError } from "h3";
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 createError({
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 createError({
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, createError } from "h3";
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 createError({
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
- 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;
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.20.0",
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": "npm run dev:prepare && nuxi dev playground",
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.1",
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.25",
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, createError } from 'h3'
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 createError({
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, createError } from 'h3'
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 createError({
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 createError({
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, createError } from 'h3'
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 createError({
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
- if (config.foreignKeys.length > 0) {
67
- const tableRelations: Record<string, string> = {}
68
- relations[tableName] = tableRelations
103
+ const tableRelations: Record<string, string> = {}
104
+ relations[tableName] = tableRelations
69
105
 
70
- // Map column names to property names
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 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
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