nuxt-auto-crud 1.5.0 → 1.7.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.
Files changed (33) hide show
  1. package/README.md +29 -9
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +18 -4
  4. package/dist/runtime/composables/useRelationDisplay.d.ts +12 -0
  5. package/dist/runtime/composables/useRelationDisplay.js +48 -0
  6. package/dist/runtime/composables/useResourceSchemas.d.ts +19 -0
  7. package/dist/runtime/composables/useResourceSchemas.js +17 -0
  8. package/dist/runtime/server/api/[model]/[id].patch.js +3 -0
  9. package/dist/runtime/server/api/[model]/index.get.js +2 -1
  10. package/dist/runtime/server/api/_relations.get.d.ts +2 -0
  11. package/dist/runtime/server/api/_relations.get.js +24 -0
  12. package/dist/runtime/server/api/_schema/[table].get.d.ts +10 -0
  13. package/dist/runtime/server/api/_schema/[table].get.js +32 -0
  14. package/dist/runtime/server/api/_schema/index.get.d.ts +2 -0
  15. package/dist/runtime/server/api/_schema/index.get.js +24 -0
  16. package/dist/runtime/server/plugins/seed.d.ts +2 -0
  17. package/dist/runtime/server/plugins/seed.js +29 -0
  18. package/dist/runtime/server/utils/auth.js +1 -3
  19. package/dist/runtime/server/utils/modelMapper.js +16 -8
  20. package/dist/runtime/server/utils/schema.d.ts +20 -0
  21. package/dist/runtime/server/utils/schema.js +70 -0
  22. package/package.json +5 -2
  23. package/src/runtime/composables/useRelationDisplay.ts +67 -0
  24. package/src/runtime/composables/useResourceSchemas.ts +42 -0
  25. package/src/runtime/server/api/[model]/[id].patch.ts +5 -0
  26. package/src/runtime/server/api/[model]/index.get.ts +5 -2
  27. package/src/runtime/server/api/_relations.get.ts +31 -0
  28. package/src/runtime/server/api/_schema/[table].get.ts +41 -0
  29. package/src/runtime/server/api/_schema/index.get.ts +31 -0
  30. package/src/runtime/server/plugins/seed.ts +42 -0
  31. package/src/runtime/server/utils/auth.ts +3 -7
  32. package/src/runtime/server/utils/modelMapper.ts +25 -11
  33. package/src/runtime/server/utils/schema.ts +96 -0
package/README.md CHANGED
@@ -4,11 +4,21 @@
4
4
 
5
5
  Auto-generate RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. Minimal configuration required.
6
6
 
7
+ **Core Philosophy:**
8
+ The main objective of this module is to **expose CRUD APIs without the need for writing code**. You define your database schema, and `nuxt-auto-crud` handles the rest.
9
+
10
+ You don't need to setup an extra server or database to create an MVP of an application. The Nuxt (Nitro) server and SQLite can save you time and money.
11
+ And you don't need a separate Strapi or Supabase setup to automate your CRUD process. `nuxt-auto-crud` will help you with that and accelerate your development exponentially.
12
+
13
+ While we provide a playground with a CMS-like interface, this is primarily to demonstrate the capabilities. You are expected to build your own frontend application to consume these APIs.
14
+
7
15
  - [✨ Release Notes](/CHANGELOG.md)
8
16
  - [🎮 Try the Playground](/playground)
9
17
 
10
18
  ## 🚀 CRUD APIs are ready to use without code
11
19
 
20
+ Once installed, your database tables automatically become API endpoints:
21
+
12
22
  - `GET /api/:model` - List all records
13
23
  - `POST /api/:model` - Create a new record
14
24
  - `GET /api/:model/:id` - Get record by ID
@@ -29,6 +39,11 @@ bun db:generate
29
39
  bun run dev
30
40
  ```
31
41
 
42
+ **Template Usage Modes:**
43
+
44
+ 1. **Fullstack App**: The template includes the `nuxt-auto-crud` module, providing both the backend APIs and the frontend UI.
45
+ 2. **Frontend Only**: You can use the template just for the frontend. In this case, you don't need to install the module in the frontend app. Instead, you would install `nuxt-auto-crud` in a separate backend setup (e.g., another Nuxt project acting as the API).
46
+
32
47
  Detailed instructions can be found in [https://auto-crud.clifland.in/](https://auto-crud.clifland.in/)
33
48
 
34
49
  ### 2. Manual Setup (Existing Project)
@@ -78,6 +93,7 @@ Add the generation script to your `package.json`:
78
93
  "scripts": {
79
94
  "db:generate": "drizzle-kit generate"
80
95
  }
96
+ // ...
81
97
  }
82
98
  ```
83
99
 
@@ -89,7 +105,7 @@ import { defineConfig } from 'drizzle-kit'
89
105
 
90
106
  export default defineConfig({
91
107
  dialect: 'sqlite',
92
- schema: './server/database/schema.ts',
108
+ schema: './server/database/schema/index.ts', // Point to your schema index file
93
109
  out: './server/database/migrations'
94
110
  })
95
111
  ```
@@ -116,10 +132,10 @@ export type User = typeof schema.users.$inferSelect
116
132
 
117
133
  #### Define your database schema
118
134
 
119
- Create `server/database/schema.ts`:
135
+ Create your schema files in `server/database/schema/`. For example, `server/database/schema/users.ts`:
120
136
 
121
137
  ```typescript
122
- // server/database/schema.ts
138
+ // server/database/schema/users.ts
123
139
  import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
124
140
 
125
141
  export const users = sqliteTable('users', {
@@ -132,6 +148,8 @@ export const users = sqliteTable('users', {
132
148
  })
133
149
  ```
134
150
 
151
+ > **Note:** The `organization.ts` and `cms.ts` files you might see in the playground are just examples and are commented out by default. You should implement a robust schema tailored to your production needs.
152
+
135
153
  #### Run the project
136
154
 
137
155
  ```bash
@@ -217,6 +235,10 @@ export default {
217
235
  }
218
236
  ```
219
237
 
238
+ ## ⚠️ Known Issues
239
+
240
+ - **Foreign Key Naming:** Currently, if you have multiple foreign keys referring to the same table (e.g., `customer_id` and `author_id` both referring to the `users` table), the automatic relation handling might assume `user_id` for both. This is a known limitation in the current alpha version.
241
+
220
242
  ## 🎮 Try the Playground
221
243
 
222
244
  Want to see it in action? Clone this repo and try the playground:
@@ -234,12 +256,6 @@ cd playground
234
256
  bun install
235
257
  bun db:generate
236
258
  bun run dev
237
-
238
- # Run the playground (Backend Only)
239
- cd playground-backendonly
240
- bun install
241
- bun db:generate
242
- bun run dev
243
259
  ```
244
260
 
245
261
  ## 📖 Usage Examples
@@ -285,6 +301,10 @@ const updated = await $fetch("/api/users/1", {
285
301
  ```typescript
286
302
  await $fetch("/api/users/1", {
287
303
  method: "DELETE",
304
+ headers: {
305
+ // If auth is enabled
306
+ Authorization: 'Bearer ...'
307
+ }
288
308
  });
289
309
  ```
290
310
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.5.0",
4
+ "version": "1.7.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir, addImportsDir, addServerPlugin } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -74,6 +74,21 @@ const module$1 = defineNuxtModule({
74
74
  resources: mergedResources || {}
75
75
  };
76
76
  const apiDir = resolver.resolve("./runtime/server/api");
77
+ addServerHandler({
78
+ route: "/api/_schema",
79
+ method: "get",
80
+ handler: resolver.resolve(apiDir, "_schema/index.get")
81
+ });
82
+ addServerHandler({
83
+ route: "/api/_schema/:table",
84
+ method: "get",
85
+ handler: resolver.resolve(apiDir, "_schema/[table].get")
86
+ });
87
+ addServerHandler({
88
+ route: "/api/_relations",
89
+ method: "get",
90
+ handler: resolver.resolve(apiDir, "_relations.get")
91
+ });
77
92
  addServerHandler({
78
93
  route: "/api/:model",
79
94
  method: "get",
@@ -100,9 +115,8 @@ const module$1 = defineNuxtModule({
100
115
  handler: resolver.resolve(apiDir, "[model]/[id].delete")
101
116
  });
102
117
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
103
- console.log("\u{1F680} Auto CRUD module loaded!");
104
- console.log(` - Schema: ${options.schemaPath}`);
105
- console.log(` - API: /api/[model]`);
118
+ addImportsDir(resolver.resolve("./runtime/composables"));
119
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/seed"));
106
120
  }
107
121
  });
108
122
 
@@ -0,0 +1,12 @@
1
+ export declare const useRelationDisplay: (schema: {
2
+ resource: string;
3
+ fields: {
4
+ name: string;
5
+ type: string;
6
+ required?: boolean;
7
+ }[];
8
+ }) => {
9
+ fetchRelations: () => Promise<void>;
10
+ getDisplayValue: (key: string, value: unknown) => unknown;
11
+ relationsMap: import("vue").Ref<Record<string, Record<string, string>>, Record<string, Record<string, string>>>;
12
+ };
@@ -0,0 +1,48 @@
1
+ import { ref, useFetch, useRequestHeaders } from "#imports";
2
+ export const useRelationDisplay = (schema) => {
3
+ const resourceName = schema.resource;
4
+ const relationsMap = ref({});
5
+ const displayValues = ref({});
6
+ const headers = useRequestHeaders(["cookie"]);
7
+ const fetchRelations = async () => {
8
+ const { data: relations } = await useFetch("/api/_relations");
9
+ if (relations.value) {
10
+ relationsMap.value = relations.value;
11
+ }
12
+ const resourceRelations = relationsMap.value[resourceName] || {};
13
+ const relationFields = Object.keys(resourceRelations);
14
+ if (relationFields.length === 0) return;
15
+ await Promise.all(
16
+ relationFields.map(async (fieldName) => {
17
+ const targetTable = resourceRelations[fieldName];
18
+ try {
19
+ const relatedData = await $fetch(`/api/${targetTable}`, { headers });
20
+ if (relatedData) {
21
+ displayValues.value[fieldName] = relatedData.reduce(
22
+ (acc, item) => {
23
+ const id = item.id;
24
+ const label = item.name || item.title || item.email || item.username || `#${item.id}`;
25
+ acc[id] = label;
26
+ return acc;
27
+ },
28
+ {}
29
+ );
30
+ }
31
+ } catch (error) {
32
+ console.error(`Failed to fetch relation data for ${targetTable}:`, error);
33
+ }
34
+ })
35
+ );
36
+ };
37
+ const getDisplayValue = (key, value) => {
38
+ if (displayValues.value[key] && (typeof value === "number" || typeof value === "string")) {
39
+ return displayValues.value[key][value] || value;
40
+ }
41
+ return value;
42
+ };
43
+ return {
44
+ fetchRelations,
45
+ getDisplayValue,
46
+ relationsMap
47
+ };
48
+ };
@@ -0,0 +1,19 @@
1
+ import type { Ref } from 'vue';
2
+ export interface ResourceField {
3
+ name: string;
4
+ type: string;
5
+ required?: boolean;
6
+ selectOptions?: string[];
7
+ }
8
+ export interface ResourceSchema {
9
+ resource: string;
10
+ fields: ResourceField[];
11
+ }
12
+ export type ResourceSchemas = Record<string, ResourceSchema>;
13
+ export declare const useResourceSchemas: () => Promise<{
14
+ schemas: Ref<ResourceSchemas | null | undefined>;
15
+ getSchema: (resource: string) => ResourceSchema | undefined;
16
+ status: Ref<"idle" | "pending" | "success" | "error">;
17
+ error: Ref<any>;
18
+ refresh: () => Promise<void>;
19
+ }>;
@@ -0,0 +1,17 @@
1
+ import { useAsyncData, useRequestHeaders } from "#imports";
2
+ export const useResourceSchemas = async () => {
3
+ const { data: schemas, status, error, refresh } = await useAsyncData("resource-schemas", () => $fetch("/api/_schema", {
4
+ headers: useRequestHeaders(["cookie"])
5
+ }));
6
+ const getSchema = (resource) => {
7
+ if (!schemas.value) return void 0;
8
+ return schemas.value[resource];
9
+ };
10
+ return {
11
+ schemas,
12
+ getSchema,
13
+ status,
14
+ error,
15
+ refresh
16
+ };
17
+ };
@@ -21,6 +21,9 @@ export default eventHandler(async (event) => {
21
21
  const table = getTableForModel(model);
22
22
  const body = await readBody(event);
23
23
  const payload = filterUpdatableFields(model, body);
24
+ if ("updatedAt" in table) {
25
+ payload.updatedAt = /* @__PURE__ */ new Date();
26
+ }
24
27
  const updatedRecord = await useDrizzle().update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
25
28
  if (!updatedRecord) {
26
29
  throw createError({
@@ -3,6 +3,7 @@ import { getTableForModel, filterHiddenFields, filterPublicColumns } from "../..
3
3
  import { useDrizzle } from "#site/drizzle";
4
4
  import { useAutoCrudConfig } from "../../utils/config.js";
5
5
  import { checkAdminAccess } from "../../utils/auth.js";
6
+ import { desc } from "drizzle-orm";
6
7
  export default eventHandler(async (event) => {
7
8
  console.log("[GET] Request received", event.path);
8
9
  const { resources } = useAutoCrudConfig();
@@ -19,7 +20,7 @@ export default eventHandler(async (event) => {
19
20
  }
20
21
  }
21
22
  const table = getTableForModel(model);
22
- const results = await useDrizzle().select().from(table).all();
23
+ const results = await useDrizzle().select().from(table).orderBy(desc(table.id)).all();
23
24
  return results.map((item) => {
24
25
  if (isAdmin) {
25
26
  return filterHiddenFields(model, item);
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, Record<string, string>>>>;
2
+ export default _default;
@@ -0,0 +1,24 @@
1
+ import { eventHandler, createError } from "h3";
2
+ import { getRelations } from "../utils/schema.js";
3
+ import { useAutoCrudConfig } from "../utils/config.js";
4
+ import { verifyJwtToken } from "../utils/jwt.js";
5
+ export default eventHandler(async (event) => {
6
+ const { auth } = useAutoCrudConfig();
7
+ if (auth?.authentication) {
8
+ let isAuthenticated = false;
9
+ if (auth.type === "jwt" && auth.jwtSecret) {
10
+ isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
11
+ } else {
12
+ try {
13
+ await requireUserSession(event);
14
+ isAuthenticated = true;
15
+ } catch {
16
+ isAuthenticated = false;
17
+ }
18
+ }
19
+ if (!isAuthenticated) {
20
+ throw createError({ statusCode: 401, message: "Unauthorized" });
21
+ }
22
+ }
23
+ return getRelations();
24
+ });
@@ -0,0 +1,10 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ resource: string;
3
+ fields: {
4
+ name: string;
5
+ type: string;
6
+ required: any;
7
+ selectOptions: undefined;
8
+ }[];
9
+ }>>;
10
+ export default _default;
@@ -0,0 +1,32 @@
1
+ import { eventHandler, createError, getRouterParam } from "h3";
2
+ import { getSchema } from "../../utils/schema.js";
3
+ import { useAutoCrudConfig } from "../../utils/config.js";
4
+ import { verifyJwtToken } from "../../utils/jwt.js";
5
+ export default eventHandler(async (event) => {
6
+ const { auth } = useAutoCrudConfig();
7
+ const tableName = getRouterParam(event, "table");
8
+ if (auth?.authentication) {
9
+ let isAuthenticated = false;
10
+ if (auth.type === "jwt" && auth.jwtSecret) {
11
+ isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
12
+ } else {
13
+ try {
14
+ await requireUserSession(event);
15
+ isAuthenticated = true;
16
+ } catch {
17
+ isAuthenticated = false;
18
+ }
19
+ }
20
+ if (!isAuthenticated) {
21
+ throw createError({ statusCode: 401, message: "Unauthorized" });
22
+ }
23
+ }
24
+ if (!tableName) {
25
+ throw createError({ statusCode: 400, message: "Table name is required" });
26
+ }
27
+ const schema = await getSchema(tableName);
28
+ if (!schema) {
29
+ throw createError({ statusCode: 404, message: `Table '${tableName}' not found` });
30
+ }
31
+ return schema;
32
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, any>>>;
2
+ export default _default;
@@ -0,0 +1,24 @@
1
+ import { eventHandler, createError } from "h3";
2
+ import { getAllSchemas } from "../../utils/schema.js";
3
+ import { useAutoCrudConfig } from "../../utils/config.js";
4
+ import { verifyJwtToken } from "../../utils/jwt.js";
5
+ export default eventHandler(async (event) => {
6
+ const { auth } = useAutoCrudConfig();
7
+ if (auth?.authentication) {
8
+ let isAuthenticated = false;
9
+ if (auth.type === "jwt" && auth.jwtSecret) {
10
+ isAuthenticated = await verifyJwtToken(event, auth.jwtSecret);
11
+ } else {
12
+ try {
13
+ await requireUserSession(event);
14
+ isAuthenticated = true;
15
+ } catch {
16
+ isAuthenticated = false;
17
+ }
18
+ }
19
+ if (!isAuthenticated) {
20
+ throw createError({ statusCode: 401, message: "Unauthorized" });
21
+ }
22
+ }
23
+ return getAllSchemas();
24
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,29 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { useDrizzle } from "#site/drizzle";
3
+ import * as tables from "#site/schema";
4
+ import { useAutoCrudConfig } from "../utils/config.js";
5
+ import { defineNitroPlugin, hashPassword } from "#imports";
6
+ export default defineNitroPlugin(async () => {
7
+ onHubReady(async () => {
8
+ const { auth } = useAutoCrudConfig();
9
+ if (!auth?.authentication || !tables.users) {
10
+ return;
11
+ }
12
+ const db = useDrizzle();
13
+ const existingAdmin = await db.select().from(tables.users).where(eq(tables.users.email, "admin@example.com")).get();
14
+ if (!existingAdmin) {
15
+ console.log("Seeding admin user...");
16
+ const hashedPassword = await hashPassword("$1Password");
17
+ await db.insert(tables.users).values({
18
+ email: "admin@example.com",
19
+ password: hashedPassword,
20
+ name: "Admin User",
21
+ avatar: "https://i.pravatar.cc/150?u=admin",
22
+ role: "admin",
23
+ createdAt: /* @__PURE__ */ new Date(),
24
+ updatedAt: /* @__PURE__ */ new Date()
25
+ });
26
+ console.log("Admin user seeded.");
27
+ }
28
+ });
29
+ });
@@ -1,4 +1,5 @@
1
1
  import { createError } from "h3";
2
+ import { requireUserSession } from "#imports";
2
3
  import { useAutoCrudConfig } from "./config.js";
3
4
  import { verifyJwtToken } from "./jwt.js";
4
5
  export async function checkAdminAccess(event, model, action) {
@@ -13,9 +14,6 @@ export async function checkAdminAccess(event, model, action) {
13
14
  }
14
15
  return verifyJwtToken(event, auth.jwtSecret);
15
16
  }
16
- if (typeof requireUserSession !== "function") {
17
- throw new TypeError("requireUserSession is not available");
18
- }
19
17
  try {
20
18
  await requireUserSession(event);
21
19
  if (auth.authorization) {
@@ -1,10 +1,10 @@
1
1
  import * as schema from "#site/schema";
2
2
  import pluralize from "pluralize";
3
3
  import { pascalCase } from "scule";
4
- import { getTableColumns as getDrizzleTableColumns } from "drizzle-orm";
4
+ import { getTableColumns as getDrizzleTableColumns, getTableName } from "drizzle-orm";
5
5
  import { createError } from "h3";
6
6
  import { useRuntimeConfig } from "#imports";
7
- const PROTECTED_FIELDS = ["id", "createdAt", "created_at"];
7
+ const PROTECTED_FIELDS = ["id", "created_at", "updated_at", "createdAt", "updatedAt"];
8
8
  const HIDDEN_FIELDS = ["password", "secret", "token"];
9
9
  export const customUpdatableFields = {
10
10
  // Add custom field restrictions here if needed
@@ -17,11 +17,12 @@ function buildModelTableMap() {
17
17
  const tableMap = {};
18
18
  for (const [key, value] of Object.entries(schema)) {
19
19
  if (value && typeof value === "object") {
20
- const hasTableSymbol = Symbol.for("drizzle:Name") in value;
21
- const hasUnderscore = "_" in value;
22
- const hasTableConfig = "table" in value || "$inferSelect" in value;
23
- if (hasTableSymbol || hasUnderscore || hasTableConfig) {
24
- tableMap[key] = value;
20
+ try {
21
+ const tableName = getTableName(value);
22
+ if (tableName) {
23
+ tableMap[key] = value;
24
+ }
25
+ } catch {
25
26
  }
26
27
  }
27
28
  }
@@ -60,9 +61,16 @@ export function getUpdatableFields(modelName) {
60
61
  export function filterUpdatableFields(modelName, data) {
61
62
  const allowedFields = getUpdatableFields(modelName);
62
63
  const filtered = {};
64
+ const table = modelTableMap[modelName];
65
+ const columns = table ? getDrizzleTableColumns(table) : {};
63
66
  for (const field of allowedFields) {
64
67
  if (data[field] !== void 0) {
65
- filtered[field] = data[field];
68
+ let value = data[field];
69
+ const column = columns[field];
70
+ if (column && column.mode === "timestamp" && typeof value === "string") {
71
+ value = new Date(value);
72
+ }
73
+ filtered[field] = value;
66
74
  }
67
75
  }
68
76
  return filtered;
@@ -0,0 +1,20 @@
1
+ export declare function drizzleTableToFields(table: any, resourceName: string): {
2
+ resource: string;
3
+ fields: {
4
+ name: string;
5
+ type: string;
6
+ required: any;
7
+ selectOptions: undefined;
8
+ }[];
9
+ };
10
+ export declare function getRelations(): Promise<Record<string, Record<string, string>>>;
11
+ export declare function getAllSchemas(): Promise<Record<string, any>>;
12
+ export declare function getSchema(tableName: string): Promise<{
13
+ resource: string;
14
+ fields: {
15
+ name: string;
16
+ type: string;
17
+ required: any;
18
+ selectOptions: undefined;
19
+ }[];
20
+ } | undefined>;
@@ -0,0 +1,70 @@
1
+ import { getTableColumns } from "drizzle-orm";
2
+ import { getTableConfig } from "drizzle-orm/sqlite-core";
3
+ import { modelTableMap } from "./modelMapper.js";
4
+ export function drizzleTableToFields(table, resourceName) {
5
+ const columns = getTableColumns(table);
6
+ const fields = [];
7
+ for (const [key, col] of Object.entries(columns)) {
8
+ const column = col;
9
+ const isRequired = column.notNull;
10
+ let type = "string";
11
+ const selectOptions = void 0;
12
+ if (column.dataType === "number" || column.columnType === "SQLiteInteger" || column.columnType === "SQLiteReal") {
13
+ type = "number";
14
+ if (column.name.endsWith("_at") || column.name.endsWith("At")) {
15
+ type = "date";
16
+ }
17
+ } else if (column.dataType === "boolean") {
18
+ type = "boolean";
19
+ } else if (column.dataType === "date" || column.dataType === "string" && (column.name.endsWith("_at") || column.name.endsWith("At"))) {
20
+ type = "date";
21
+ }
22
+ fields.push({
23
+ name: key,
24
+ type,
25
+ required: isRequired,
26
+ selectOptions
27
+ });
28
+ }
29
+ return {
30
+ resource: resourceName,
31
+ fields
32
+ };
33
+ }
34
+ export async function getRelations() {
35
+ const relations = {};
36
+ for (const [tableName, table] of Object.entries(modelTableMap)) {
37
+ try {
38
+ const config = getTableConfig(table);
39
+ if (config.foreignKeys.length > 0) {
40
+ const tableRelations = {};
41
+ relations[tableName] = tableRelations;
42
+ const columns = getTableColumns(table);
43
+ const columnToProperty = {};
44
+ for (const [key, col] of Object.entries(columns)) {
45
+ columnToProperty[col.name] = key;
46
+ }
47
+ config.foreignKeys.forEach((fk) => {
48
+ const sourceColumnName = fk.reference().columns[0].name;
49
+ const sourceProperty = columnToProperty[sourceColumnName] || sourceColumnName;
50
+ const targetTable = fk.reference().foreignTable[Symbol.for("drizzle:Name")];
51
+ tableRelations[sourceProperty] = targetTable;
52
+ });
53
+ }
54
+ } catch {
55
+ }
56
+ }
57
+ return relations;
58
+ }
59
+ export async function getAllSchemas() {
60
+ const schemas = {};
61
+ for (const [tableName, table] of Object.entries(modelTableMap)) {
62
+ schemas[tableName] = drizzleTableToFields(table, tableName);
63
+ }
64
+ return schemas;
65
+ }
66
+ export async function getSchema(tableName) {
67
+ const table = modelTableMap[tableName];
68
+ if (!table) return void 0;
69
+ return drizzleTableToFields(table, tableName);
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",
@@ -55,9 +55,12 @@
55
55
  "jose": "^5.9.6"
56
56
  },
57
57
  "peerDependencies": {
58
- "drizzle-orm": "^0.30.0"
58
+ "drizzle-orm": ">=0.30.0"
59
59
  },
60
60
  "devDependencies": {
61
+ "@iconify-json/heroicons": "^1.2.3",
62
+ "@iconify-json/lucide": "^1.2.77",
63
+ "@iconify-json/simple-icons": "^1.2.61",
61
64
  "@nuxt/devtools": "^3.1.0",
62
65
  "@nuxt/eslint-config": "^1.10.0",
63
66
  "@nuxt/module-builder": "^1.0.2",
@@ -0,0 +1,67 @@
1
+ import { ref, useFetch, useRequestHeaders } from '#imports'
2
+
3
+ export const useRelationDisplay = (
4
+ schema: {
5
+ resource: string
6
+ fields: { name: string, type: string, required?: boolean }[]
7
+ },
8
+ ) => {
9
+ const resourceName = schema.resource
10
+ const relationsMap = ref<Record<string, Record<string, string>>>({})
11
+ const displayValues = ref<Record<string, Record<string, string>>>({})
12
+ const headers = useRequestHeaders(['cookie'])
13
+
14
+ const fetchRelations = async () => {
15
+ // 1. Fetch relations metadata
16
+ const { data: relations } = await useFetch<Record<string, Record<string, string>>>('/api/_relations')
17
+ if (relations.value) {
18
+ relationsMap.value = relations.value
19
+ }
20
+
21
+ // 2. Identify relation fields for this resource
22
+ const resourceRelations = relationsMap.value[resourceName] || {}
23
+ const relationFields = Object.keys(resourceRelations)
24
+
25
+ if (relationFields.length === 0) return
26
+
27
+ // 3. Fetch data for each relation
28
+ await Promise.all(
29
+ relationFields.map(async (fieldName) => {
30
+ const targetTable = resourceRelations[fieldName]
31
+ // We assume the API for targetTable is /api/[targetTable]
32
+ try {
33
+ const relatedData = await $fetch<Record<string, unknown>[]>(`/api/${targetTable}`, { headers })
34
+
35
+ if (relatedData) {
36
+ displayValues.value[fieldName] = relatedData.reduce<Record<string, string>>(
37
+ (acc, item) => {
38
+ const id = item.id as number
39
+ // Try to find a good display name
40
+ const label = (item.name || item.title || item.email || item.username || `#${item.id}`) as string
41
+ acc[id] = label
42
+ return acc
43
+ },
44
+ {},
45
+ )
46
+ }
47
+ }
48
+ catch (error) {
49
+ console.error(`Failed to fetch relation data for ${targetTable}:`, error)
50
+ }
51
+ }),
52
+ )
53
+ }
54
+
55
+ const getDisplayValue = (key: string, value: unknown) => {
56
+ if (displayValues.value[key] && (typeof value === 'number' || typeof value === 'string')) {
57
+ return displayValues.value[key][value as string] || value
58
+ }
59
+ return value
60
+ }
61
+
62
+ return {
63
+ fetchRelations,
64
+ getDisplayValue,
65
+ relationsMap,
66
+ }
67
+ }
@@ -0,0 +1,42 @@
1
+ import { useAsyncData, useRequestHeaders } from '#imports'
2
+ import type { Ref } from 'vue'
3
+
4
+ export interface ResourceField {
5
+ name: string
6
+ type: string
7
+ required?: boolean
8
+ selectOptions?: string[]
9
+ }
10
+
11
+ export interface ResourceSchema {
12
+ resource: string
13
+ fields: ResourceField[]
14
+ }
15
+
16
+ export type ResourceSchemas = Record<string, ResourceSchema>
17
+
18
+ export const useResourceSchemas = async (): Promise<{
19
+ schemas: Ref<ResourceSchemas | null | undefined>
20
+ getSchema: (resource: string) => ResourceSchema | undefined
21
+ status: Ref<'idle' | 'pending' | 'success' | 'error'>
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ error: Ref<any>
24
+ refresh: () => Promise<void>
25
+ }> => {
26
+ const { data: schemas, status, error, refresh } = await useAsyncData<ResourceSchemas>('resource-schemas', () => $fetch('/api/_schema', {
27
+ headers: useRequestHeaders(['cookie']),
28
+ }))
29
+
30
+ const getSchema = (resource: string) => {
31
+ if (!schemas.value) return undefined
32
+ return schemas.value[resource]
33
+ }
34
+
35
+ return {
36
+ schemas,
37
+ getSchema,
38
+ status,
39
+ error,
40
+ refresh: refresh as unknown as () => Promise<void>,
41
+ }
42
+ }
@@ -34,6 +34,11 @@ export default eventHandler(async (event) => {
34
34
  const body = await readBody(event)
35
35
  const payload = filterUpdatableFields(model, body)
36
36
 
37
+ // Automatically update updatedAt if it exists
38
+ if ('updatedAt' in table) {
39
+ payload.updatedAt = new Date()
40
+ }
41
+
37
42
  const updatedRecord = await useDrizzle()
38
43
  .update(table)
39
44
  .set(payload)
@@ -8,6 +8,9 @@ import { useDrizzle } from '#site/drizzle'
8
8
  import { useAutoCrudConfig } from '../../utils/config'
9
9
  import { checkAdminAccess } from '../../utils/auth'
10
10
 
11
+ import { desc } from 'drizzle-orm'
12
+ import type { TableWithId } from '../../types'
13
+
11
14
  export default eventHandler(async (event) => {
12
15
  console.log('[GET] Request received', event.path)
13
16
  const { resources } = useAutoCrudConfig()
@@ -28,9 +31,9 @@ export default eventHandler(async (event) => {
28
31
  }
29
32
  }
30
33
 
31
- const table = getTableForModel(model)
34
+ const table = getTableForModel(model) as TableWithId
32
35
 
33
- const results = await useDrizzle().select().from(table).all()
36
+ const results = await useDrizzle().select().from(table).orderBy(desc(table.id)).all()
34
37
 
35
38
  return results.map((item: Record<string, unknown>) => {
36
39
  if (isAdmin) {
@@ -0,0 +1,31 @@
1
+ import { eventHandler, createError } from 'h3'
2
+ import { getRelations } from '../utils/schema'
3
+ import { useAutoCrudConfig } from '../utils/config'
4
+ import { verifyJwtToken } from '../utils/jwt'
5
+
6
+ export default eventHandler(async (event) => {
7
+ const { auth } = useAutoCrudConfig()
8
+
9
+ if (auth?.authentication) {
10
+ let isAuthenticated = false
11
+ if (auth.type === 'jwt' && auth.jwtSecret) {
12
+ isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
13
+ }
14
+ else {
15
+ try {
16
+ // @ts-expect-error - requireUserSession is auto-imported
17
+ await requireUserSession(event)
18
+ isAuthenticated = true
19
+ }
20
+ catch {
21
+ isAuthenticated = false
22
+ }
23
+ }
24
+
25
+ if (!isAuthenticated) {
26
+ throw createError({ statusCode: 401, message: 'Unauthorized' })
27
+ }
28
+ }
29
+
30
+ return getRelations()
31
+ })
@@ -0,0 +1,41 @@
1
+ import { eventHandler, createError, getRouterParam } from 'h3'
2
+ import { getSchema } from '../../utils/schema'
3
+ import { useAutoCrudConfig } from '../../utils/config'
4
+ import { verifyJwtToken } from '../../utils/jwt'
5
+
6
+ export default eventHandler(async (event) => {
7
+ const { auth } = useAutoCrudConfig()
8
+ const tableName = getRouterParam(event, 'table')
9
+
10
+ if (auth?.authentication) {
11
+ let isAuthenticated = false
12
+ if (auth.type === 'jwt' && auth.jwtSecret) {
13
+ isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
14
+ }
15
+ else {
16
+ try {
17
+ // @ts-expect-error - requireUserSession is auto-imported
18
+ await requireUserSession(event)
19
+ isAuthenticated = true
20
+ }
21
+ catch {
22
+ isAuthenticated = false
23
+ }
24
+ }
25
+
26
+ if (!isAuthenticated) {
27
+ throw createError({ statusCode: 401, message: 'Unauthorized' })
28
+ }
29
+ }
30
+
31
+ if (!tableName) {
32
+ throw createError({ statusCode: 400, message: 'Table name is required' })
33
+ }
34
+
35
+ const schema = await getSchema(tableName)
36
+ if (!schema) {
37
+ throw createError({ statusCode: 404, message: `Table '${tableName}' not found` })
38
+ }
39
+
40
+ return schema
41
+ })
@@ -0,0 +1,31 @@
1
+ import { eventHandler, createError } from 'h3'
2
+ import { getAllSchemas } from '../../utils/schema'
3
+ import { useAutoCrudConfig } from '../../utils/config'
4
+ import { verifyJwtToken } from '../../utils/jwt'
5
+
6
+ export default eventHandler(async (event) => {
7
+ const { auth } = useAutoCrudConfig()
8
+
9
+ if (auth?.authentication) {
10
+ let isAuthenticated = false
11
+ if (auth.type === 'jwt' && auth.jwtSecret) {
12
+ isAuthenticated = await verifyJwtToken(event, auth.jwtSecret)
13
+ }
14
+ else {
15
+ try {
16
+ // @ts-expect-error - requireUserSession is auto-imported
17
+ await requireUserSession(event)
18
+ isAuthenticated = true
19
+ }
20
+ catch {
21
+ isAuthenticated = false
22
+ }
23
+ }
24
+
25
+ if (!isAuthenticated) {
26
+ throw createError({ statusCode: 401, message: 'Unauthorized' })
27
+ }
28
+ }
29
+
30
+ return getAllSchemas()
31
+ })
@@ -0,0 +1,42 @@
1
+ import { eq } from 'drizzle-orm'
2
+ // @ts-expect-error - #site/drizzle is an alias defined by the module
3
+ import { useDrizzle } from '#site/drizzle'
4
+ // @ts-expect-error - #site/schema is an alias defined by the module
5
+ import * as tables from '#site/schema'
6
+ import { useAutoCrudConfig } from '../utils/config'
7
+ // @ts-expect-error - #imports is available in runtime
8
+ import { defineNitroPlugin, hashPassword } from '#imports'
9
+
10
+ export default defineNitroPlugin(async () => {
11
+ // @ts-expect-error - onHubReady is auto-imported from @nuxthub/core
12
+ onHubReady(async () => {
13
+ const { auth } = useAutoCrudConfig()
14
+
15
+ // Only seed if auth is enabled and we have a users table
16
+ if (!auth?.authentication || !tables.users) {
17
+ return
18
+ }
19
+
20
+ const db = useDrizzle()
21
+
22
+ // Check if admin exists
23
+ const existingAdmin = await db.select().from(tables.users).where(eq(tables.users.email, 'admin@example.com')).get()
24
+
25
+ if (!existingAdmin) {
26
+ console.log('Seeding admin user...')
27
+
28
+ const hashedPassword = await hashPassword('$1Password')
29
+
30
+ await db.insert(tables.users).values({
31
+ email: 'admin@example.com',
32
+ password: hashedPassword,
33
+ name: 'Admin User',
34
+ avatar: 'https://i.pravatar.cc/150?u=admin',
35
+ role: 'admin',
36
+ createdAt: new Date(),
37
+ updatedAt: new Date(),
38
+ })
39
+ console.log('Admin user seeded.')
40
+ }
41
+ })
42
+ })
@@ -1,5 +1,7 @@
1
1
  import type { H3Event } from 'h3'
2
2
  import { createError } from 'h3'
3
+ // @ts-expect-error - #imports is available in runtime
4
+ import { requireUserSession } from '#imports'
3
5
  import { useAutoCrudConfig } from './config'
4
6
  import { verifyJwtToken } from './jwt'
5
7
 
@@ -19,13 +21,7 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
19
21
  }
20
22
 
21
23
  // Session based (default)
22
- // @ts-expect-error - requireUserSession is auto-imported
23
- if (typeof requireUserSession !== 'function') {
24
- throw new TypeError('requireUserSession is not available')
25
- }
26
-
27
24
  try {
28
- // @ts-expect-error - requireUserSession is auto-imported
29
25
  await requireUserSession(event)
30
26
 
31
27
  // Check authorization if enabled
@@ -49,7 +45,7 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
49
45
  if ((e as any).statusCode === 403) {
50
46
  throw e
51
47
  }
52
- // Otherwise (401 from requireUserSession), return false (treat as guest)
48
+ // Otherwise (401 from requireUserSession or function not available), return false (treat as guest)
53
49
  return false
54
50
  }
55
51
  }
@@ -3,7 +3,7 @@
3
3
  import * as schema from '#site/schema'
4
4
  import pluralize from 'pluralize'
5
5
  import { pascalCase } from 'scule'
6
- import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
6
+ import { getTableColumns as getDrizzleTableColumns, getTableName } from 'drizzle-orm'
7
7
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
8
8
  import { createError } from 'h3'
9
9
  import { useRuntimeConfig } from '#imports'
@@ -11,7 +11,7 @@ import { useRuntimeConfig } from '#imports'
11
11
  /**
12
12
  * Fields that should never be updatable via PATCH requests
13
13
  */
14
- const PROTECTED_FIELDS = ['id', 'createdAt', 'created_at']
14
+ const PROTECTED_FIELDS = ['id', 'created_at', 'updated_at', 'createdAt', 'updatedAt']
15
15
 
16
16
  /**
17
17
  * Fields that should never be returned in API responses
@@ -50,15 +50,18 @@ function buildModelTableMap(): Record<string, unknown> {
50
50
  // Iterate through all exports from schema
51
51
  for (const [key, value] of Object.entries(schema)) {
52
52
  // Check if it's a Drizzle table
53
- // Drizzle tables have specific properties we can check
54
53
  if (value && typeof value === 'object') {
55
- // Check for common Drizzle table properties
56
- const hasTableSymbol = Symbol.for('drizzle:Name') in value
57
- const hasUnderscore = '_' in value
58
- const hasTableConfig = 'table' in value || '$inferSelect' in value
59
-
60
- if (hasTableSymbol || hasUnderscore || hasTableConfig) {
61
- tableMap[key] = value
54
+ try {
55
+ // getTableName returns the table name for valid tables, and undefined/null for others (like relations)
56
+ // This is a more robust check than checking for properties
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ const tableName = getTableName(value as any)
59
+ if (tableName) {
60
+ tableMap[key] = value
61
+ }
62
+ }
63
+ catch {
64
+ // Ignore if it throws (not a table)
62
65
  }
63
66
  }
64
67
  }
@@ -139,10 +142,21 @@ export function getUpdatableFields(modelName: string): string[] {
139
142
  export function filterUpdatableFields(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
140
143
  const allowedFields = getUpdatableFields(modelName)
141
144
  const filtered: Record<string, unknown> = {}
145
+ const table = modelTableMap[modelName]
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ const columns = table ? getDrizzleTableColumns(table as any) : {}
142
148
 
143
149
  for (const field of allowedFields) {
144
150
  if (data[field] !== undefined) {
145
- filtered[field] = data[field]
151
+ let value = data[field]
152
+ const column = columns[field]
153
+
154
+ // Coerce timestamp fields to Date objects if they are strings
155
+ if (column && column.mode === 'timestamp' && typeof value === 'string') {
156
+ value = new Date(value)
157
+ }
158
+
159
+ filtered[field] = value
146
160
  }
147
161
  }
148
162
 
@@ -0,0 +1,96 @@
1
+ import { getTableColumns } from 'drizzle-orm'
2
+ import { getTableConfig } from 'drizzle-orm/sqlite-core'
3
+ import { modelTableMap } from './modelMapper'
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export function drizzleTableToFields(table: any, resourceName: string) {
7
+ const columns = getTableColumns(table)
8
+ const fields = []
9
+
10
+ for (const [key, col] of Object.entries(columns)) {
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ const column = col as any
13
+ const isRequired = column.notNull
14
+ let type = 'string'
15
+ const selectOptions: string[] | undefined = undefined
16
+
17
+ // Map Drizzle types to frontend types
18
+ if (column.dataType === 'number' || column.columnType === 'SQLiteInteger' || column.columnType === 'SQLiteReal') {
19
+ type = 'number'
20
+ // Check if it is a timestamp
21
+ if (column.name.endsWith('_at') || column.name.endsWith('At')) {
22
+ type = 'date'
23
+ }
24
+ }
25
+ else if (column.dataType === 'boolean') {
26
+ type = 'boolean'
27
+ }
28
+ else if (column.dataType === 'date' || (column.dataType === 'string' && (column.name.endsWith('_at') || column.name.endsWith('At')))) {
29
+ type = 'date'
30
+ }
31
+
32
+ fields.push({
33
+ name: key,
34
+ type,
35
+ required: isRequired,
36
+ selectOptions,
37
+ })
38
+ }
39
+
40
+ return {
41
+ resource: resourceName,
42
+ fields,
43
+ }
44
+ }
45
+
46
+ export async function getRelations() {
47
+ const relations: Record<string, Record<string, string>> = {}
48
+
49
+ for (const [tableName, table] of Object.entries(modelTableMap)) {
50
+ try {
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const config = getTableConfig(table as any)
53
+ if (config.foreignKeys.length > 0) {
54
+ const tableRelations: Record<string, string> = {}
55
+ relations[tableName] = tableRelations
56
+
57
+ // Map column names to property names
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const columns = getTableColumns(table as any)
60
+ const columnToProperty: Record<string, string> = {}
61
+ for (const [key, col] of Object.entries(columns)) {
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ columnToProperty[(col as any).name] = key
64
+ }
65
+
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ config.foreignKeys.forEach((fk: any) => {
68
+ const sourceColumnName = fk.reference().columns[0].name
69
+ const sourceProperty = columnToProperty[sourceColumnName] || sourceColumnName
70
+ const targetTable = fk.reference().foreignTable[Symbol.for('drizzle:Name')]
71
+ tableRelations[sourceProperty] = targetTable
72
+ })
73
+ }
74
+ }
75
+ catch {
76
+ // Ignore tables that don't have config (e.g. not Drizzle tables)
77
+ }
78
+ }
79
+ return relations
80
+ }
81
+
82
+ export async function getAllSchemas() {
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ const schemas: Record<string, any> = {}
85
+
86
+ for (const [tableName, table] of Object.entries(modelTableMap)) {
87
+ schemas[tableName] = drizzleTableToFields(table, tableName)
88
+ }
89
+ return schemas
90
+ }
91
+
92
+ export async function getSchema(tableName: string) {
93
+ const table = modelTableMap[tableName]
94
+ if (!table) return undefined
95
+ return drizzleTableToFields(table, tableName)
96
+ }