nuxt-auto-crud 1.1.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 +232 -0
- package/dist/module.d.mts +13 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +51 -0
- package/dist/runtime/server/api/[model]/[id].delete.d.ts +2 -0
- package/dist/runtime/server/api/[model]/[id].delete.js +13 -0
- package/dist/runtime/server/api/[model]/[id].get.d.ts +2 -0
- package/dist/runtime/server/api/[model]/[id].get.js +13 -0
- package/dist/runtime/server/api/[model]/[id].patch.d.ts +2 -0
- package/dist/runtime/server/api/[model]/[id].patch.js +8 -0
- package/dist/runtime/server/api/[model]/index.get.d.ts +2 -0
- package/dist/runtime/server/api/[model]/index.get.js +6 -0
- package/dist/runtime/server/api/[model]/index.post.d.ts +2 -0
- package/dist/runtime/server/api/[model]/index.post.js +11 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/modelMapper.d.ts +53 -0
- package/dist/runtime/server/utils/modelMapper.js +74 -0
- package/dist/types.d.mts +3 -0
- package/package.json +71 -0
- package/src/runtime/server/api/[model]/[id].delete.ts +21 -0
- package/src/runtime/server/api/[model]/[id].get.ts +21 -0
- package/src/runtime/server/api/[model]/[id].patch.ts +18 -0
- package/src/runtime/server/api/[model]/index.get.ts +9 -0
- package/src/runtime/server/api/[model]/index.post.ts +20 -0
- package/src/runtime/server/tsconfig.json +3 -0
- package/src/runtime/server/utils/modelMapper.ts +163 -0
package/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Nuxt Auto CRUD
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![License][license-src]][license-href]
|
|
6
|
+
[![Nuxt][nuxt-src]][nuxt-href]
|
|
7
|
+
|
|
8
|
+
Auto-generate RESTful CRUD APIs for your Nuxt app based solely on your database schema. No configuration needed!
|
|
9
|
+
|
|
10
|
+
- [✨ Release Notes](/CHANGELOG.md)
|
|
11
|
+
- [🎮 Try the Playground](/playground)
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- 🔄 **Auto-Detection** - Automatically detects all tables from your Drizzle schema
|
|
16
|
+
- 🚀 **Zero Configuration** - Just define your schema, APIs are generated automatically
|
|
17
|
+
- 🛡️ **Protected Fields** - Automatically protects `id` and `createdAt` fields from updates
|
|
18
|
+
- 📝 **Full CRUD** - Complete Create, Read, Update, Delete operations out of the box
|
|
19
|
+
- 🎯 **Type-Safe** - Fully typed with TypeScript support
|
|
20
|
+
- 🔌 **Works with NuxtHub** - Seamlessly integrates with NuxtHub database
|
|
21
|
+
|
|
22
|
+
## 📦 Quick Setup
|
|
23
|
+
|
|
24
|
+
### 1. Install the module
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun add nuxt-auto-crud
|
|
28
|
+
# or
|
|
29
|
+
npm install nuxt-auto-crud
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Add to your Nuxt config
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// nuxt.config.ts
|
|
36
|
+
export default defineNuxtConfig({
|
|
37
|
+
modules: ["@nuxthub/core", "nuxt-auto-crud"],
|
|
38
|
+
|
|
39
|
+
hub: {
|
|
40
|
+
database: true,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
autoCrud: {
|
|
44
|
+
schemaPath: "server/database/schema", // default value
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Define your database schema
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// server/database/schema.ts
|
|
53
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
54
|
+
|
|
55
|
+
export const users = sqliteTable("users", {
|
|
56
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
57
|
+
name: text("name").notNull(),
|
|
58
|
+
email: text("email").notNull().unique(),
|
|
59
|
+
bio: text("bio"),
|
|
60
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
61
|
+
() => new Date()
|
|
62
|
+
),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const posts = sqliteTable("posts", {
|
|
66
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
67
|
+
title: text("title").notNull(),
|
|
68
|
+
content: text("content").notNull(),
|
|
69
|
+
published: integer("published", { mode: "boolean" }).default(false),
|
|
70
|
+
authorId: integer("author_id").references(() => users.id),
|
|
71
|
+
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
72
|
+
() => new Date()
|
|
73
|
+
),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's it! 🎉 Your CRUD APIs are now available:
|
|
78
|
+
|
|
79
|
+
- `GET /api/users` - List all users
|
|
80
|
+
- `POST /api/users` - Create a new user
|
|
81
|
+
- `GET /api/users/:id` - Get user by ID
|
|
82
|
+
- `PATCH /api/users/:id` - Update user
|
|
83
|
+
- `DELETE /api/users/:id` - Delete user
|
|
84
|
+
|
|
85
|
+
_(Same endpoints for all your tables!)_
|
|
86
|
+
|
|
87
|
+
## 🎮 Try the Playground
|
|
88
|
+
|
|
89
|
+
Want to see it in action? Clone this repo and try the playground:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Clone the repository
|
|
93
|
+
git clone https://github.com/clifordpereira/nuxt-auto-crud.git
|
|
94
|
+
cd nuxt-auto-crud
|
|
95
|
+
|
|
96
|
+
# Install dependencies
|
|
97
|
+
bun install
|
|
98
|
+
|
|
99
|
+
# Run the playground
|
|
100
|
+
cd playground
|
|
101
|
+
bun install
|
|
102
|
+
bun run dev
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The playground includes a sample schema with users, posts, and comments tables, plus an interactive UI to explore all the features.
|
|
106
|
+
|
|
107
|
+
## 📖 Usage Examples
|
|
108
|
+
|
|
109
|
+
### Create a Record
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const user = await $fetch("/api/users", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
body: {
|
|
115
|
+
name: "John Doe",
|
|
116
|
+
email: "john@example.com",
|
|
117
|
+
bio: "Software developer",
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Get All Records
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const users = await $fetch("/api/users");
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Get Record by ID
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const user = await $fetch("/api/users/1");
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Update a Record
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const updated = await $fetch("/api/users/1", {
|
|
138
|
+
method: "PATCH",
|
|
139
|
+
body: {
|
|
140
|
+
bio: "Updated bio",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Delete a Record
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
await $fetch("/api/users/1", {
|
|
149
|
+
method: "DELETE",
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## ⚙️ Configuration
|
|
154
|
+
|
|
155
|
+
### Module Options
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
export default defineNuxtConfig({
|
|
159
|
+
autoCrud: {
|
|
160
|
+
// Path to your database schema file (relative to project root)
|
|
161
|
+
schemaPath: "server/database/schema", // default
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Protected Fields
|
|
167
|
+
|
|
168
|
+
By default, the following fields are protected from updates:
|
|
169
|
+
|
|
170
|
+
- `id`
|
|
171
|
+
- `createdAt`
|
|
172
|
+
- `created_at`
|
|
173
|
+
|
|
174
|
+
You can customize updatable fields in your schema by modifying the `modelMapper.ts` utility.
|
|
175
|
+
|
|
176
|
+
## 🔧 Requirements
|
|
177
|
+
|
|
178
|
+
- Nuxt 3 or 4
|
|
179
|
+
- NuxtHub (for database functionality)
|
|
180
|
+
- Drizzle ORM
|
|
181
|
+
|
|
182
|
+
## 🤝 Contributing
|
|
183
|
+
|
|
184
|
+
Contributions are welcome! Please check out the [contribution guide](/CONTRIBUTING.md).
|
|
185
|
+
|
|
186
|
+
<details>
|
|
187
|
+
<summary>Local development</summary>
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Install dependencies
|
|
191
|
+
bun install
|
|
192
|
+
|
|
193
|
+
# Generate type stubs
|
|
194
|
+
bun run dev:prepare
|
|
195
|
+
|
|
196
|
+
# Develop with the playground
|
|
197
|
+
bun run dev
|
|
198
|
+
|
|
199
|
+
# Build the playground
|
|
200
|
+
bun run dev:build
|
|
201
|
+
|
|
202
|
+
# Run ESLint
|
|
203
|
+
bun run lint
|
|
204
|
+
|
|
205
|
+
# Run Vitest
|
|
206
|
+
bun run test
|
|
207
|
+
bun run test:watch
|
|
208
|
+
|
|
209
|
+
# Release new version
|
|
210
|
+
bun run release
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
</details>
|
|
214
|
+
|
|
215
|
+
## 📝 License
|
|
216
|
+
|
|
217
|
+
[MIT License](./LICENSE)
|
|
218
|
+
|
|
219
|
+
## 👨💻 Author
|
|
220
|
+
|
|
221
|
+
Made with ❤️ by [Cliford Pereira](https://github.com/clifordpereira)
|
|
222
|
+
|
|
223
|
+
<!-- Badges -->
|
|
224
|
+
|
|
225
|
+
[npm-version-src]: https://img.shields.io/npm/v/nuxt-auto-crud/latest.svg?style=flat&colorA=020420&colorB=00DC82
|
|
226
|
+
[npm-version-href]: https://npmjs.com/package/nuxt-auto-crud
|
|
227
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-auto-crud.svg?style=flat&colorA=020420&colorB=00DC82
|
|
228
|
+
[npm-downloads-href]: https://npm.chart.dev/nuxt-auto-crud
|
|
229
|
+
[license-src]: https://img.shields.io/npm/l/nuxt-auto-crud.svg?style=flat&colorA=020420&colorB=00DC82
|
|
230
|
+
[license-href]: https://npmjs.com/package/nuxt-auto-crud
|
|
231
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
|
|
232
|
+
[nuxt-href]: https://nuxt.com
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Path to the database schema file
|
|
6
|
+
* @default 'server/database/schema'
|
|
7
|
+
*/
|
|
8
|
+
schemaPath?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
11
|
+
|
|
12
|
+
export { _default as default };
|
|
13
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerHandler, addServerImportsDir } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
const module$1 = defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "nuxt-auto-crud",
|
|
6
|
+
configKey: "autoCrud"
|
|
7
|
+
},
|
|
8
|
+
defaults: {
|
|
9
|
+
schemaPath: "server/database/schema"
|
|
10
|
+
},
|
|
11
|
+
setup(options, nuxt) {
|
|
12
|
+
const resolver = createResolver(import.meta.url);
|
|
13
|
+
const schemaPath = resolver.resolve(
|
|
14
|
+
nuxt.options.rootDir,
|
|
15
|
+
options.schemaPath
|
|
16
|
+
);
|
|
17
|
+
nuxt.options.alias["#site/schema"] = schemaPath;
|
|
18
|
+
const apiDir = resolver.resolve("./runtime/server/api");
|
|
19
|
+
addServerHandler({
|
|
20
|
+
route: "/api/:model",
|
|
21
|
+
method: "get",
|
|
22
|
+
handler: resolver.resolve(apiDir, "[model]/index.get.ts")
|
|
23
|
+
});
|
|
24
|
+
addServerHandler({
|
|
25
|
+
route: "/api/:model",
|
|
26
|
+
method: "post",
|
|
27
|
+
handler: resolver.resolve(apiDir, "[model]/index.post.ts")
|
|
28
|
+
});
|
|
29
|
+
addServerHandler({
|
|
30
|
+
route: "/api/:model/:id",
|
|
31
|
+
method: "get",
|
|
32
|
+
handler: resolver.resolve(apiDir, "[model]/[id].get.ts")
|
|
33
|
+
});
|
|
34
|
+
addServerHandler({
|
|
35
|
+
route: "/api/:model/:id",
|
|
36
|
+
method: "patch",
|
|
37
|
+
handler: resolver.resolve(apiDir, "[model]/[id].patch.ts")
|
|
38
|
+
});
|
|
39
|
+
addServerHandler({
|
|
40
|
+
route: "/api/:model/:id",
|
|
41
|
+
method: "delete",
|
|
42
|
+
handler: resolver.resolve(apiDir, "[model]/[id].delete.ts")
|
|
43
|
+
});
|
|
44
|
+
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
45
|
+
console.log("\u{1F680} Auto CRUD module loaded!");
|
|
46
|
+
console.log(` - Schema: ${options.schemaPath}`);
|
|
47
|
+
console.log(` - API: /api/[model]`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default eventHandler(async (event) => {
|
|
2
|
+
const { model, id } = getRouterParams(event);
|
|
3
|
+
const table = getTableForModel(model);
|
|
4
|
+
const singularName = getModelSingularName(model);
|
|
5
|
+
const deletedRecord = await useDrizzle().delete(table).where(eq(table.id, Number(id))).returning().get();
|
|
6
|
+
if (!deletedRecord) {
|
|
7
|
+
throw createError({
|
|
8
|
+
statusCode: 404,
|
|
9
|
+
message: `${singularName} not found`
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return deletedRecord;
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default eventHandler(async (event) => {
|
|
2
|
+
const { model, id } = getRouterParams(event);
|
|
3
|
+
const table = getTableForModel(model);
|
|
4
|
+
const singularName = getModelSingularName(model);
|
|
5
|
+
const record = await useDrizzle().select().from(table).where(eq(table.id, Number(id))).get();
|
|
6
|
+
if (!record) {
|
|
7
|
+
throw createError({
|
|
8
|
+
statusCode: 404,
|
|
9
|
+
message: `${singularName} not found`
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return record;
|
|
13
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default eventHandler(async (event) => {
|
|
2
|
+
const { model, id } = getRouterParams(event);
|
|
3
|
+
const table = getTableForModel(model);
|
|
4
|
+
const body = await readBody(event);
|
|
5
|
+
const updateData = filterUpdatableFields(model, body);
|
|
6
|
+
const record = await useDrizzle().update(table).set(updateData).where(eq(table.id, Number(id))).returning().get();
|
|
7
|
+
return record;
|
|
8
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default eventHandler(async (event) => {
|
|
2
|
+
const { model } = getRouterParams(event);
|
|
3
|
+
const table = getTableForModel(model);
|
|
4
|
+
const body = await readBody(event);
|
|
5
|
+
const values = {
|
|
6
|
+
...body,
|
|
7
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
8
|
+
};
|
|
9
|
+
const record = await useDrizzle().insert(table).values(values).returning().get();
|
|
10
|
+
return record;
|
|
11
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom updatable fields configuration (optional)
|
|
3
|
+
* Only define here if you want to override the auto-detection
|
|
4
|
+
*
|
|
5
|
+
* Example:
|
|
6
|
+
* export const customUpdatableFields: Record<string, string[]> = {
|
|
7
|
+
* users: ['name', 'avatar'], // Only these fields can be updated
|
|
8
|
+
* }
|
|
9
|
+
*/
|
|
10
|
+
export declare const customUpdatableFields: Record<string, string[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Auto-generated model table map
|
|
13
|
+
* Automatically includes all tables from schema
|
|
14
|
+
*/
|
|
15
|
+
export declare const modelTableMap: Record<string, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Gets the table for a given model name
|
|
18
|
+
* @param modelName - The name of the model (e.g., 'users', 'products')
|
|
19
|
+
* @returns The corresponding database table
|
|
20
|
+
* @throws Error if model is not found
|
|
21
|
+
*/
|
|
22
|
+
export declare function getTableForModel(modelName: string): {};
|
|
23
|
+
/**
|
|
24
|
+
* Gets the updatable fields for a model
|
|
25
|
+
* @param modelName - The name of the model
|
|
26
|
+
* @returns Array of field names that can be updated
|
|
27
|
+
*/
|
|
28
|
+
export declare function getUpdatableFields(modelName: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Filters an object to only include updatable fields for a model
|
|
31
|
+
* @param modelName - The name of the model
|
|
32
|
+
* @param data - The data object to filter
|
|
33
|
+
* @returns Filtered object with only updatable fields
|
|
34
|
+
*/
|
|
35
|
+
export declare function filterUpdatableFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Gets the singular name for a model (for error messages)
|
|
38
|
+
* Uses pluralize library for accurate singular/plural conversion
|
|
39
|
+
* @param modelName - The plural model name
|
|
40
|
+
* @returns The singular name in PascalCase
|
|
41
|
+
*/
|
|
42
|
+
export declare function getModelSingularName(modelName: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Gets the plural name for a model
|
|
45
|
+
* @param modelName - The model name (singular or plural)
|
|
46
|
+
* @returns The plural name
|
|
47
|
+
*/
|
|
48
|
+
export declare function getModelPluralName(modelName: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Lists all available models
|
|
51
|
+
* @returns Array of model names
|
|
52
|
+
*/
|
|
53
|
+
export declare function getAvailableModels(): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as schema from "#site/schema";
|
|
2
|
+
import pluralize from "pluralize";
|
|
3
|
+
import { pascalCase } from "scule";
|
|
4
|
+
import { getTableColumns as getDrizzleTableColumns } from "drizzle-orm";
|
|
5
|
+
import { createError } from "h3";
|
|
6
|
+
const PROTECTED_FIELDS = ["id", "createdAt", "created_at"];
|
|
7
|
+
export const customUpdatableFields = {
|
|
8
|
+
// Add custom field restrictions here if needed
|
|
9
|
+
// By default, all fields except PROTECTED_FIELDS are updatable
|
|
10
|
+
};
|
|
11
|
+
function buildModelTableMap() {
|
|
12
|
+
const tableMap = {};
|
|
13
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
const hasTableSymbol = Symbol.for("drizzle:Name") in value;
|
|
16
|
+
const hasUnderscore = "_" in value;
|
|
17
|
+
const hasTableConfig = "table" in value || "$inferSelect" in value;
|
|
18
|
+
if (hasTableSymbol || hasUnderscore || hasTableConfig) {
|
|
19
|
+
tableMap[key] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return tableMap;
|
|
24
|
+
}
|
|
25
|
+
export const modelTableMap = buildModelTableMap();
|
|
26
|
+
export function getTableForModel(modelName) {
|
|
27
|
+
const table = modelTableMap[modelName];
|
|
28
|
+
if (!table) {
|
|
29
|
+
const availableModels = Object.keys(modelTableMap).join(", ");
|
|
30
|
+
throw createError({
|
|
31
|
+
statusCode: 404,
|
|
32
|
+
message: `Model '${modelName}' not found. Available models: ${availableModels}`
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return table;
|
|
36
|
+
}
|
|
37
|
+
function getTableColumns(table) {
|
|
38
|
+
try {
|
|
39
|
+
const columns = getDrizzleTableColumns(table);
|
|
40
|
+
return Object.keys(columns);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error("[getTableColumns] Error getting columns:", e);
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function getUpdatableFields(modelName) {
|
|
47
|
+
if (customUpdatableFields[modelName]) {
|
|
48
|
+
return customUpdatableFields[modelName];
|
|
49
|
+
}
|
|
50
|
+
const table = modelTableMap[modelName];
|
|
51
|
+
if (!table) return [];
|
|
52
|
+
const allColumns = getTableColumns(table);
|
|
53
|
+
return allColumns.filter((col) => !PROTECTED_FIELDS.includes(col));
|
|
54
|
+
}
|
|
55
|
+
export function filterUpdatableFields(modelName, data) {
|
|
56
|
+
const allowedFields = getUpdatableFields(modelName);
|
|
57
|
+
const filtered = {};
|
|
58
|
+
for (const field of allowedFields) {
|
|
59
|
+
if (data[field] !== void 0) {
|
|
60
|
+
filtered[field] = data[field];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return filtered;
|
|
64
|
+
}
|
|
65
|
+
export function getModelSingularName(modelName) {
|
|
66
|
+
const singular = pluralize.singular(modelName);
|
|
67
|
+
return pascalCase(singular);
|
|
68
|
+
}
|
|
69
|
+
export function getModelPluralName(modelName) {
|
|
70
|
+
return pluralize.plural(modelName);
|
|
71
|
+
}
|
|
72
|
+
export function getAvailableModels() {
|
|
73
|
+
return Object.keys(modelTableMap);
|
|
74
|
+
}
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-auto-crud",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Exposes RESTful CRUD APIs for your Nuxt app based solely on your database migrations.",
|
|
5
|
+
"author": "Cliford Pereira",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nuxt",
|
|
9
|
+
"nuxt-module",
|
|
10
|
+
"crud",
|
|
11
|
+
"auto-crud"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/clifordpereira/nuxt-auto-crud"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/types.d.mts",
|
|
21
|
+
"import": "./dist/module.mjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/module.mjs",
|
|
25
|
+
"typesVersions": {
|
|
26
|
+
"*": {
|
|
27
|
+
".": [
|
|
28
|
+
"./dist/types.d.mts"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src/runtime"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepack": "nuxt-module-build build",
|
|
38
|
+
"dev": "npm run dev:prepare && nuxi dev playground",
|
|
39
|
+
"dev:build": "nuxi build playground",
|
|
40
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
41
|
+
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest watch",
|
|
45
|
+
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
46
|
+
"link": "npm link"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@nuxt/kit": "^4.2.1",
|
|
50
|
+
"@types/pluralize": "^0.0.33"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"drizzle-orm": "^0.30.0",
|
|
54
|
+
"pluralize": "^8.0.0",
|
|
55
|
+
"scule": "^1.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@nuxt/devtools": "^3.1.0",
|
|
59
|
+
"@nuxt/eslint-config": "^1.10.0",
|
|
60
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
61
|
+
"@nuxt/schema": "^4.2.1",
|
|
62
|
+
"@nuxt/test-utils": "^3.20.1",
|
|
63
|
+
"@types/node": "latest",
|
|
64
|
+
"changelogen": "^0.6.2",
|
|
65
|
+
"eslint": "^9.39.1",
|
|
66
|
+
"nuxt": "^4.2.1",
|
|
67
|
+
"typescript": "~5.9.3",
|
|
68
|
+
"vitest": "^4.0.13",
|
|
69
|
+
"vue-tsc": "^3.1.5"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// server/api/[model]/[id].delete.ts
|
|
2
|
+
export default eventHandler(async (event) => {
|
|
3
|
+
const { model, id } = getRouterParams(event)
|
|
4
|
+
const table = getTableForModel(model)
|
|
5
|
+
const singularName = getModelSingularName(model)
|
|
6
|
+
|
|
7
|
+
const deletedRecord = await useDrizzle()
|
|
8
|
+
.delete(table)
|
|
9
|
+
.where(eq(table.id, Number(id)))
|
|
10
|
+
.returning()
|
|
11
|
+
.get()
|
|
12
|
+
|
|
13
|
+
if (!deletedRecord) {
|
|
14
|
+
throw createError({
|
|
15
|
+
statusCode: 404,
|
|
16
|
+
message: `${singularName} not found`,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return deletedRecord
|
|
21
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// server/api/[model]/[id].get.ts
|
|
2
|
+
export default eventHandler(async (event) => {
|
|
3
|
+
const { model, id } = getRouterParams(event)
|
|
4
|
+
const table = getTableForModel(model)
|
|
5
|
+
const singularName = getModelSingularName(model)
|
|
6
|
+
|
|
7
|
+
const record = await useDrizzle()
|
|
8
|
+
.select()
|
|
9
|
+
.from(table)
|
|
10
|
+
.where(eq(table.id, Number(id)))
|
|
11
|
+
.get()
|
|
12
|
+
|
|
13
|
+
if (!record) {
|
|
14
|
+
throw createError({
|
|
15
|
+
statusCode: 404,
|
|
16
|
+
message: `${singularName} not found`,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return record
|
|
21
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// server/api/[model]/[id].patch.ts
|
|
2
|
+
export default eventHandler(async (event) => {
|
|
3
|
+
const { model, id } = getRouterParams(event)
|
|
4
|
+
const table = getTableForModel(model)
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
|
|
7
|
+
// Filter to only allow updatable fields for this model
|
|
8
|
+
const updateData = filterUpdatableFields(model, body)
|
|
9
|
+
|
|
10
|
+
const record = await useDrizzle()
|
|
11
|
+
.update(table)
|
|
12
|
+
.set(updateData)
|
|
13
|
+
.where(eq(table.id, Number(id)))
|
|
14
|
+
.returning()
|
|
15
|
+
.get()
|
|
16
|
+
|
|
17
|
+
return record
|
|
18
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// server/api/[model]/index.post.ts
|
|
2
|
+
export default eventHandler(async (event) => {
|
|
3
|
+
const { model } = getRouterParams(event)
|
|
4
|
+
const table = getTableForModel(model)
|
|
5
|
+
const body = await readBody(event)
|
|
6
|
+
|
|
7
|
+
// Add createdAt timestamp
|
|
8
|
+
const values = {
|
|
9
|
+
...body,
|
|
10
|
+
createdAt: new Date(),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const record = await useDrizzle()
|
|
14
|
+
.insert(table)
|
|
15
|
+
.values(values)
|
|
16
|
+
.returning()
|
|
17
|
+
.get()
|
|
18
|
+
|
|
19
|
+
return record
|
|
20
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// runtime/server/utils/modelMapper.ts
|
|
2
|
+
// @ts-expect-error - #site/schema is an alias defined by the module
|
|
3
|
+
import * as schema from '#site/schema'
|
|
4
|
+
import pluralize from 'pluralize'
|
|
5
|
+
import { pascalCase } from 'scule'
|
|
6
|
+
import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
|
|
7
|
+
import { createError } from 'h3'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fields that should never be updatable via PATCH requests
|
|
11
|
+
*/
|
|
12
|
+
const PROTECTED_FIELDS = ['id', 'createdAt', 'created_at']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Custom updatable fields configuration (optional)
|
|
16
|
+
* Only define here if you want to override the auto-detection
|
|
17
|
+
*
|
|
18
|
+
* Example:
|
|
19
|
+
* export const customUpdatableFields: Record<string, string[]> = {
|
|
20
|
+
* users: ['name', 'avatar'], // Only these fields can be updated
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
export const customUpdatableFields: Record<string, string[]> = {
|
|
24
|
+
// Add custom field restrictions here if needed
|
|
25
|
+
// By default, all fields except PROTECTED_FIELDS are updatable
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Automatically builds a map of all exported tables from the schema
|
|
30
|
+
* No manual configuration needed!
|
|
31
|
+
*/
|
|
32
|
+
function buildModelTableMap(): Record<string, unknown> {
|
|
33
|
+
const tableMap: Record<string, unknown> = {}
|
|
34
|
+
|
|
35
|
+
// Iterate through all exports from schema
|
|
36
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
37
|
+
// Check if it's a Drizzle table
|
|
38
|
+
// Drizzle tables have specific properties we can check
|
|
39
|
+
if (value && typeof value === 'object') {
|
|
40
|
+
// Check for common Drizzle table properties
|
|
41
|
+
const hasTableSymbol = Symbol.for('drizzle:Name') in value
|
|
42
|
+
const hasUnderscore = '_' in value
|
|
43
|
+
const hasTableConfig = 'table' in value || '$inferSelect' in value
|
|
44
|
+
|
|
45
|
+
if (hasTableSymbol || hasUnderscore || hasTableConfig) {
|
|
46
|
+
tableMap[key] = value
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return tableMap
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Auto-generated model table map
|
|
56
|
+
* Automatically includes all tables from schema
|
|
57
|
+
*/
|
|
58
|
+
export const modelTableMap = buildModelTableMap()
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gets the table for a given model name
|
|
62
|
+
* @param modelName - The name of the model (e.g., 'users', 'products')
|
|
63
|
+
* @returns The corresponding database table
|
|
64
|
+
* @throws Error if model is not found
|
|
65
|
+
*/
|
|
66
|
+
export function getTableForModel(modelName: string) {
|
|
67
|
+
const table = modelTableMap[modelName]
|
|
68
|
+
|
|
69
|
+
if (!table) {
|
|
70
|
+
const availableModels = Object.keys(modelTableMap).join(', ')
|
|
71
|
+
throw createError({
|
|
72
|
+
statusCode: 404,
|
|
73
|
+
message: `Model '${modelName}' not found. Available models: ${availableModels}`,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return table
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Auto-detects updatable fields for a table
|
|
82
|
+
* Returns all fields except protected ones (id, createdAt, etc.)
|
|
83
|
+
*/
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
function getTableColumns(table: any): string[] {
|
|
86
|
+
try {
|
|
87
|
+
const columns = getDrizzleTableColumns(table)
|
|
88
|
+
return Object.keys(columns)
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.error('[getTableColumns] Error getting columns:', e)
|
|
92
|
+
return []
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gets the updatable fields for a model
|
|
98
|
+
* @param modelName - The name of the model
|
|
99
|
+
* @returns Array of field names that can be updated
|
|
100
|
+
*/
|
|
101
|
+
export function getUpdatableFields(modelName: string): string[] {
|
|
102
|
+
// Check if custom fields are defined for this model
|
|
103
|
+
if (customUpdatableFields[modelName]) {
|
|
104
|
+
return customUpdatableFields[modelName]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Auto-detect from table schema
|
|
108
|
+
const table = modelTableMap[modelName]
|
|
109
|
+
|
|
110
|
+
if (!table) return []
|
|
111
|
+
|
|
112
|
+
const allColumns = getTableColumns(table)
|
|
113
|
+
|
|
114
|
+
// Filter out protected fields
|
|
115
|
+
return allColumns.filter(col => !PROTECTED_FIELDS.includes(col))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Filters an object to only include updatable fields for a model
|
|
120
|
+
* @param modelName - The name of the model
|
|
121
|
+
* @param data - The data object to filter
|
|
122
|
+
* @returns Filtered object with only updatable fields
|
|
123
|
+
*/
|
|
124
|
+
export function filterUpdatableFields(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
|
|
125
|
+
const allowedFields = getUpdatableFields(modelName)
|
|
126
|
+
const filtered: Record<string, unknown> = {}
|
|
127
|
+
|
|
128
|
+
for (const field of allowedFields) {
|
|
129
|
+
if (data[field] !== undefined) {
|
|
130
|
+
filtered[field] = data[field]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return filtered
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Gets the singular name for a model (for error messages)
|
|
139
|
+
* Uses pluralize library for accurate singular/plural conversion
|
|
140
|
+
* @param modelName - The plural model name
|
|
141
|
+
* @returns The singular name in PascalCase
|
|
142
|
+
*/
|
|
143
|
+
export function getModelSingularName(modelName: string): string {
|
|
144
|
+
const singular = pluralize.singular(modelName)
|
|
145
|
+
return pascalCase(singular)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Gets the plural name for a model
|
|
150
|
+
* @param modelName - The model name (singular or plural)
|
|
151
|
+
* @returns The plural name
|
|
152
|
+
*/
|
|
153
|
+
export function getModelPluralName(modelName: string): string {
|
|
154
|
+
return pluralize.plural(modelName)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Lists all available models
|
|
159
|
+
* @returns Array of model names
|
|
160
|
+
*/
|
|
161
|
+
export function getAvailableModels(): string[] {
|
|
162
|
+
return Object.keys(modelTableMap)
|
|
163
|
+
}
|