nuxt-auto-crud 1.21.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
@@ -346,12 +346,6 @@ export const posts = sqliteTable('posts', {
346
346
  })
347
347
  ```
348
348
 
349
- ## ⚠️ Known Issues
350
-
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`).
352
- - **Limitation:** If you have custom FK names like `customer_id` or `author_id` pointing to `users`, the automatic expansion will not work yet.
353
- - **Workaround:** Ensure your FK columns follow the `tablename_id` convention where possible for now.
354
-
355
349
  ## 🎮 Try the Playground
356
350
 
357
351
  Want to see it in action? Clone this repo and try the playground:
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.21.0",
4
+ "version": "1.22.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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
  });
@@ -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
+ }
@@ -19,12 +19,13 @@ export function drizzleTableToFields(table, resourceName) {
19
19
  const config = getTableConfig(table);
20
20
  config.foreignKeys.forEach((fk) => {
21
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;
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
+ }
28
29
  }
29
30
  });
30
31
  } catch {
@@ -51,6 +52,9 @@ function mapColumnType(column) {
51
52
  }
52
53
  return { type: "number" };
53
54
  }
55
+ if (["content", "description", "bio", "message"].includes(column.name)) {
56
+ return { type: "textarea" };
57
+ }
54
58
  return { type: "string" };
55
59
  }
56
60
  export async function getRelations() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.21.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,6 +78,8 @@
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.26",
82
+ "nuxt-authorization": "^0.3.5",
81
83
  "typescript": "~5.9.3",
82
84
  "vitest": "^4.0.13",
83
85
  "vue-tsc": "^3.1.5",
@@ -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
+ }
@@ -36,18 +36,18 @@ export function drizzleTableToFields(table: any, resourceName: string) {
36
36
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
37
  config.foreignKeys.forEach((fk: any) => {
38
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
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
51
  }
52
52
  })
53
53
  }
@@ -86,6 +86,10 @@ function mapColumnType(column: any): { type: string, selectOptions?: string[] }
86
86
  return { type: 'number' }
87
87
  }
88
88
 
89
+ if (['content', 'description', 'bio', 'message'].includes(column.name)) {
90
+ return { type: 'textarea' }
91
+ }
92
+
89
93
  return { type: 'string' }
90
94
  }
91
95