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 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 };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-auto-crud",
3
+ "configKey": "autoCrud",
4
+ "version": "1.1.0",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }
@@ -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,2 @@
1
+ declare const _default: any;
2
+ export default _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,2 @@
1
+ declare const _default: any;
2
+ export default _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 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,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -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,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ export default eventHandler(async (event) => {
2
+ const { model } = getRouterParams(event);
3
+ const table = getTableForModel(model);
4
+ const records = await useDrizzle().select().from(table).all();
5
+ return records;
6
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -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,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
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,9 @@
1
+ // server/api/[model]/index.get.ts
2
+ export default eventHandler(async (event) => {
3
+ const { model } = getRouterParams(event)
4
+ const table = getTableForModel(model)
5
+
6
+ const records = await useDrizzle().select().from(table).all()
7
+
8
+ return records
9
+ })
@@ -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,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -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
+ }