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.
- package/README.md +29 -9
- package/dist/module.json +1 -1
- package/dist/module.mjs +18 -4
- package/dist/runtime/composables/useRelationDisplay.d.ts +12 -0
- package/dist/runtime/composables/useRelationDisplay.js +48 -0
- package/dist/runtime/composables/useResourceSchemas.d.ts +19 -0
- package/dist/runtime/composables/useResourceSchemas.js +17 -0
- package/dist/runtime/server/api/[model]/[id].patch.js +3 -0
- package/dist/runtime/server/api/[model]/index.get.js +2 -1
- package/dist/runtime/server/api/_relations.get.d.ts +2 -0
- package/dist/runtime/server/api/_relations.get.js +24 -0
- package/dist/runtime/server/api/_schema/[table].get.d.ts +10 -0
- package/dist/runtime/server/api/_schema/[table].get.js +32 -0
- package/dist/runtime/server/api/_schema/index.get.d.ts +2 -0
- package/dist/runtime/server/api/_schema/index.get.js +24 -0
- package/dist/runtime/server/plugins/seed.d.ts +2 -0
- package/dist/runtime/server/plugins/seed.js +29 -0
- package/dist/runtime/server/utils/auth.js +1 -3
- package/dist/runtime/server/utils/modelMapper.js +16 -8
- package/dist/runtime/server/utils/schema.d.ts +20 -0
- package/dist/runtime/server/utils/schema.js +70 -0
- package/package.json +5 -2
- package/src/runtime/composables/useRelationDisplay.ts +67 -0
- package/src/runtime/composables/useResourceSchemas.ts +42 -0
- package/src/runtime/server/api/[model]/[id].patch.ts +5 -0
- package/src/runtime/server/api/[model]/index.get.ts +5 -2
- package/src/runtime/server/api/_relations.get.ts +31 -0
- package/src/runtime/server/api/_schema/[table].get.ts +41 -0
- package/src/runtime/server/api/_schema/index.get.ts +31 -0
- package/src/runtime/server/plugins/seed.ts +42 -0
- package/src/runtime/server/utils/auth.ts +3 -7
- package/src/runtime/server/utils/modelMapper.ts +25 -11
- 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
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
|
-
|
|
104
|
-
|
|
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,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,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,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,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", "
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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', '
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
+
}
|