nuxt-auto-crud 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +79 -11
  2. package/dist/module.d.mts +49 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +24 -1
  5. package/dist/runtime/server/api/[model]/[id].delete.d.ts +1 -1
  6. package/dist/runtime/server/api/[model]/[id].delete.js +20 -2
  7. package/dist/runtime/server/api/[model]/[id].get.d.ts +1 -1
  8. package/dist/runtime/server/api/[model]/[id].get.js +21 -4
  9. package/dist/runtime/server/api/[model]/[id].patch.d.ts +1 -1
  10. package/dist/runtime/server/api/[model]/[id].patch.js +29 -5
  11. package/dist/runtime/server/api/[model]/index.get.js +25 -4
  12. package/dist/runtime/server/api/[model]/index.post.d.ts +1 -1
  13. package/dist/runtime/server/api/[model]/index.post.js +23 -8
  14. package/dist/runtime/server/utils/auth.d.ts +2 -0
  15. package/dist/runtime/server/utils/auth.js +39 -0
  16. package/dist/runtime/server/utils/config.d.ts +2 -0
  17. package/dist/runtime/server/utils/config.js +4 -0
  18. package/dist/runtime/server/utils/jwt.d.ts +2 -0
  19. package/dist/runtime/server/utils/jwt.js +19 -0
  20. package/dist/runtime/server/utils/modelMapper.d.ts +31 -0
  21. package/dist/runtime/server/utils/modelMapper.js +38 -0
  22. package/package.json +17 -7
  23. package/src/runtime/server/api/[model]/[id].delete.ts +29 -3
  24. package/src/runtime/server/api/[model]/[id].get.ts +29 -5
  25. package/src/runtime/server/api/[model]/[id].patch.ts +40 -9
  26. package/src/runtime/server/api/[model]/index.get.ts +33 -5
  27. package/src/runtime/server/api/[model]/index.post.ts +32 -15
  28. package/src/runtime/server/utils/auth.ts +55 -0
  29. package/src/runtime/server/utils/config.ts +6 -0
  30. package/src/runtime/server/utils/jwt.ts +23 -0
  31. package/src/runtime/server/utils/modelMapper.ts +83 -0
package/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # Nuxt Auto CRUD
2
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
3
 
8
- Auto-generate RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. No configuration needed!
4
+
5
+ > **Note:** This module is currently in its alpha stage. However, you can use it to accelerate MVP development. It has not been tested thoroughly enough for production use; only happy-path testing is performed for each release.
6
+
7
+ Auto-generate RESTful CRUD APIs for your **Nuxt** application based solely on your database schema. Minimal configuration required.
9
8
 
10
9
  - [✨ Release Notes](/CHANGELOG.md)
11
10
  - [🎮 Try the Playground](/playground)
@@ -27,10 +26,13 @@ Start a new project with everything pre-configured using our template:
27
26
  ```bash
28
27
  npx nuxi init -t gh:clifordpereira/nuxt-auto-crud_template <project-name>
29
28
  cd <project-name>
29
+ bun install
30
30
  bun db:generate
31
31
  bun run dev
32
32
  ```
33
33
 
34
+ Detailed instructions can be found in [https://auto-crud.clifland.in/](https://auto-crud.clifland.in/)
35
+
34
36
  ### Add User
35
37
  Open Nuxt DevTools (bottom-middle icon) > `...` menu > **Database** icon to add users.
36
38
  > **Note:** If the users table doesn't appear, restart the server (`Ctrl + C` and `bun run dev`).
@@ -44,10 +46,16 @@ Visit [http://localhost:3000/api/users](http://localhost:3000/api/users).
44
46
 
45
47
  If you want to add `nuxt-auto-crud` to an existing project, follow these steps:
46
48
 
49
+ > **Note:** These instructions assume you are using NuxtHub. If you are using a custom SQLite setup (e.g. better-sqlite3, Turso), please see [Custom Setup](./custom-setup.md).
50
+
47
51
  ### 1. Install dependencies
48
52
 
49
53
  ```bash
50
54
  # Install module and required dependencies
55
+ npm install nuxt-auto-crud @nuxthub/core@latest drizzle-orm
56
+ npm install --save-dev wrangler drizzle-kit
57
+
58
+ # Or using bun
51
59
  bun add nuxt-auto-crud @nuxthub/core@latest drizzle-orm
52
60
  bun add --dev wrangler drizzle-kit
53
61
  ```
@@ -161,11 +169,11 @@ Want to see it in action? Clone this repo and try the playground:
161
169
  git clone https://github.com/clifordpereira/nuxt-auto-crud.git
162
170
  cd nuxt-auto-crud
163
171
 
164
- # Install dependencies
172
+ # Install dependencies (parent folder)
165
173
  bun install
166
174
 
167
- # Run the playground
168
- cd playground
175
+ # Run the playground (fullstack with auth)
176
+ cd playground-fullstack
169
177
  bun install
170
178
  bun db:generate
171
179
  bun run dev
@@ -219,7 +227,57 @@ await $fetch("/api/users/1", {
219
227
  });
220
228
  ```
221
229
 
222
- ## ⚙️ Configuration
230
+ ## Use Cases
231
+
232
+ ### 1. Full-stack App (with Auth)
233
+
234
+ If you are building a full-stack Nuxt application, you can easily integrate `nuxt-auth-utils` and `nuxt-authorization` to secure your auto-generated APIs.
235
+
236
+ First, install the modules:
237
+
238
+ ```bash
239
+ npx nuxi@latest module add auth-utils
240
+ npm install nuxt-authorization
241
+ ```
242
+
243
+ Then, configure `nuxt-auto-crud` in your `nuxt.config.ts`:
244
+
245
+ ```ts
246
+ export default defineNuxtConfig({
247
+ modules: [
248
+ 'nuxt-auto-crud',
249
+ 'nuxt-auth-utils'
250
+ ],
251
+ autoCrud: {
252
+ auth: {
253
+ enabled: true, // Enables requireUserSession() check
254
+ authorization: true // Enables authorize(model, action) check
255
+ }
256
+ }
257
+ })
258
+ ```
259
+
260
+ When `authorization` is enabled, the module will call `authorize(model, action)` where action is one of: `create`, `read`, `update`, `delete`.
261
+
262
+ ### 2. Backend-only App (API Mode)
263
+
264
+ If you are using Nuxt as a backend for a separate client application (e.g., mobile app, SPA), you can use this module to quickly generate REST APIs.
265
+
266
+ In this case, you might handle authentication differently (e.g., validating tokens in middleware) or disable the built-in auth checks if you have a global auth middleware.
267
+
268
+ ```ts
269
+ export default defineNuxtConfig({
270
+ modules: ['nuxt-auto-crud'],
271
+ autoCrud: {
272
+ auth: {
273
+ enabled: false, // Default
274
+ authorization: false // Default
275
+ }
276
+ }
277
+ })
278
+ ```
279
+
280
+ ## Configuration
223
281
 
224
282
  ### Module Options
225
283
 
@@ -242,11 +300,21 @@ By default, the following fields are protected from updates:
242
300
 
243
301
  You can customize updatable fields in your schema by modifying the `modelMapper.ts` utility.
244
302
 
303
+ ### Hidden Fields
304
+
305
+ By default, the following fields are hidden from API responses for security:
306
+
307
+ - `password`
308
+ - `secret`
309
+ - `token`
310
+
311
+ You can customize hidden fields by modifying the `modelMapper.ts` utility.
312
+
245
313
  ## 🔧 Requirements
246
314
 
247
315
  - Nuxt 3 or 4
248
- - NuxtHub (for database functionality)
249
- - Drizzle ORM
316
+ - Drizzle ORM (SQLite)
317
+ - NuxtHub (Recommended) or [Custom SQLite Setup](./custom-setup.md)
250
318
 
251
319
  ## 🤝 Contributing
252
320
 
package/dist/module.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  interface ModuleOptions {
4
- /**
5
4
  /**
6
5
  * Path to the database schema file
7
6
  * @default 'server/database/schema'
@@ -12,6 +11,55 @@ interface ModuleOptions {
12
11
  * @default 'server/utils/drizzle'
13
12
  */
14
13
  drizzlePath?: string;
14
+ /**
15
+ * Authentication configuration
16
+ */
17
+ auth?: {
18
+ /**
19
+ * Authentication type
20
+ * @default 'session'
21
+ */
22
+ type?: 'session' | 'jwt';
23
+ /**
24
+ * JWT Secret (required if type is 'jwt')
25
+ */
26
+ jwtSecret?: string;
27
+ /**
28
+ * Enable authentication checks (requires nuxt-auth-utils for session)
29
+ * @default false
30
+ */
31
+ enabled: boolean;
32
+ /**
33
+ * Enable authorization checks (requires nuxt-authorization)
34
+ * @default false
35
+ */
36
+ authorization?: boolean;
37
+ };
38
+ /**
39
+ * Resource-specific configuration
40
+ * Define public access and column visibility
41
+ */
42
+ resources?: {
43
+ [modelName: string]: {
44
+ /**
45
+ * Actions allowed without authentication
46
+ * true = all actions
47
+ * array = specific actions ('list', 'create', 'read', 'update', 'delete')
48
+ */
49
+ public?: boolean | ('list' | 'create' | 'read' | 'update' | 'delete')[];
50
+ /**
51
+ * Columns to return for unauthenticated requests
52
+ * If not specified, all columns (except hidden ones) are returned
53
+ */
54
+ publicColumns?: string[];
55
+ };
56
+ };
57
+ }
58
+
59
+ declare module '@nuxt/schema' {
60
+ interface RuntimeConfig {
61
+ autoCrud: ModuleOptions;
62
+ }
15
63
  }
16
64
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
17
65
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -9,7 +9,7 @@ const module$1 = defineNuxtModule({
9
9
  schemaPath: "server/database/schema",
10
10
  drizzlePath: "server/utils/drizzle"
11
11
  },
12
- setup(options, nuxt) {
12
+ async setup(options, nuxt) {
13
13
  const resolver = createResolver(import.meta.url);
14
14
  const schemaPath = resolver.resolve(
15
15
  nuxt.options.rootDir,
@@ -21,6 +21,29 @@ const module$1 = defineNuxtModule({
21
21
  options.drizzlePath
22
22
  );
23
23
  nuxt.options.alias["#site/drizzle"] = drizzlePath;
24
+ nuxt.options.alias["#authorization"] = nuxt.options.alias["#authorization"] || "nuxt-authorization/utils";
25
+ const { loadConfig } = await import('c12');
26
+ const { config: externalConfig } = await loadConfig({
27
+ name: "autocrud",
28
+ cwd: nuxt.options.rootDir
29
+ });
30
+ const mergedAuth = {
31
+ ...externalConfig?.auth,
32
+ ...options.auth
33
+ };
34
+ const mergedResources = {
35
+ ...externalConfig?.resources,
36
+ ...options.resources
37
+ };
38
+ nuxt.options.runtimeConfig.autoCrud = {
39
+ auth: {
40
+ enabled: mergedAuth.enabled ?? false,
41
+ authorization: mergedAuth.authorization ?? false,
42
+ type: mergedAuth.type ?? "session",
43
+ jwtSecret: mergedAuth.jwtSecret
44
+ },
45
+ resources: mergedResources || {}
46
+ };
24
47
  const apiDir = resolver.resolve("./runtime/server/api");
25
48
  addServerHandler({
26
49
  route: "/api/:model",
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
2
2
  export default _default;
@@ -1,9 +1,23 @@
1
1
  import { eventHandler, getRouterParams, createError } from "h3";
2
2
  import { eq } from "drizzle-orm";
3
- import { getTableForModel, getModelSingularName } from "../../utils/modelMapper.js";
3
+ import { getTableForModel, getModelSingularName, filterHiddenFields, filterPublicColumns } from "../../utils/modelMapper.js";
4
4
  import { useDrizzle } from "#site/drizzle";
5
+ import { useAutoCrudConfig } from "../../utils/config.js";
6
+ import { checkAdminAccess } from "../../utils/auth.js";
5
7
  export default eventHandler(async (event) => {
8
+ const { resources } = useAutoCrudConfig();
6
9
  const { model, id } = getRouterParams(event);
10
+ const isAdmin = await checkAdminAccess(event, model, "delete");
11
+ if (!isAdmin) {
12
+ const resourceConfig = resources?.[model];
13
+ const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("delete");
14
+ if (!isPublic) {
15
+ throw createError({
16
+ statusCode: 401,
17
+ message: "Unauthorized"
18
+ });
19
+ }
20
+ }
7
21
  const table = getTableForModel(model);
8
22
  const singularName = getModelSingularName(model);
9
23
  const deletedRecord = await useDrizzle().delete(table).where(eq(table.id, Number(id))).returning().get();
@@ -13,5 +27,9 @@ export default eventHandler(async (event) => {
13
27
  message: `${singularName} not found`
14
28
  });
15
29
  }
16
- return deletedRecord;
30
+ if (isAdmin) {
31
+ return filterHiddenFields(model, deletedRecord);
32
+ } else {
33
+ return filterPublicColumns(model, deletedRecord);
34
+ }
17
35
  });
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
2
2
  export default _default;
@@ -1,17 +1,34 @@
1
1
  import { eventHandler, getRouterParams, createError } from "h3";
2
2
  import { eq } from "drizzle-orm";
3
- import { getTableForModel, getModelSingularName } from "../../utils/modelMapper.js";
3
+ import { getTableForModel, filterHiddenFields, filterPublicColumns } from "../../utils/modelMapper.js";
4
4
  import { useDrizzle } from "#site/drizzle";
5
+ import { useAutoCrudConfig } from "../../utils/config.js";
6
+ import { checkAdminAccess } from "../../utils/auth.js";
5
7
  export default eventHandler(async (event) => {
8
+ const { resources } = useAutoCrudConfig();
6
9
  const { model, id } = getRouterParams(event);
10
+ const isAdmin = await checkAdminAccess(event, model, "read");
11
+ if (!isAdmin) {
12
+ const resourceConfig = resources?.[model];
13
+ const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("read");
14
+ if (!isPublic) {
15
+ throw createError({
16
+ statusCode: 401,
17
+ message: "Unauthorized"
18
+ });
19
+ }
20
+ }
7
21
  const table = getTableForModel(model);
8
- const singularName = getModelSingularName(model);
9
22
  const record = await useDrizzle().select().from(table).where(eq(table.id, Number(id))).get();
10
23
  if (!record) {
11
24
  throw createError({
12
25
  statusCode: 404,
13
- message: `${singularName} not found`
26
+ message: "Record not found"
14
27
  });
15
28
  }
16
- return record;
29
+ if (isAdmin) {
30
+ return filterHiddenFields(model, record);
31
+ } else {
32
+ return filterPublicColumns(model, record);
33
+ }
17
34
  });
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
2
2
  export default _default;
@@ -1,12 +1,36 @@
1
- import { eventHandler, getRouterParams, readBody } from "h3";
1
+ import { eventHandler, getRouterParams, readBody, createError } from "h3";
2
2
  import { eq } from "drizzle-orm";
3
- import { getTableForModel, filterUpdatableFields } from "../../utils/modelMapper.js";
3
+ import { getTableForModel, filterUpdatableFields, filterHiddenFields, filterPublicColumns } from "../../utils/modelMapper.js";
4
4
  import { useDrizzle } from "#site/drizzle";
5
+ import { useAutoCrudConfig } from "../../utils/config.js";
6
+ import { checkAdminAccess } from "../../utils/auth.js";
5
7
  export default eventHandler(async (event) => {
8
+ const { resources } = useAutoCrudConfig();
6
9
  const { model, id } = getRouterParams(event);
10
+ const isAdmin = await checkAdminAccess(event, model, "update");
11
+ if (!isAdmin) {
12
+ const resourceConfig = resources?.[model];
13
+ const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("update");
14
+ if (!isPublic) {
15
+ throw createError({
16
+ statusCode: 401,
17
+ message: "Unauthorized"
18
+ });
19
+ }
20
+ }
7
21
  const table = getTableForModel(model);
8
22
  const body = await readBody(event);
9
- const updateData = filterUpdatableFields(model, body);
10
- const record = await useDrizzle().update(table).set(updateData).where(eq(table.id, Number(id))).returning().get();
11
- return record;
23
+ const payload = filterUpdatableFields(model, body);
24
+ const updatedRecord = await useDrizzle().update(table).set(payload).where(eq(table.id, Number(id))).returning().get();
25
+ if (!updatedRecord) {
26
+ throw createError({
27
+ statusCode: 404,
28
+ message: "Record not found"
29
+ });
30
+ }
31
+ if (isAdmin) {
32
+ return filterHiddenFields(model, updatedRecord);
33
+ } else {
34
+ return filterPublicColumns(model, updatedRecord);
35
+ }
12
36
  });
@@ -1,9 +1,30 @@
1
- import { eventHandler, getRouterParams } from "h3";
2
- import { getTableForModel } from "../../utils/modelMapper.js";
1
+ import { eventHandler, getRouterParams, createError } from "h3";
2
+ import { getTableForModel, filterHiddenFields, filterPublicColumns } from "../../utils/modelMapper.js";
3
3
  import { useDrizzle } from "#site/drizzle";
4
+ import { useAutoCrudConfig } from "../../utils/config.js";
5
+ import { checkAdminAccess } from "../../utils/auth.js";
4
6
  export default eventHandler(async (event) => {
7
+ console.log("[GET] Request received", event.path);
8
+ const { resources } = useAutoCrudConfig();
5
9
  const { model } = getRouterParams(event);
10
+ const isAdmin = await checkAdminAccess(event, model, "list");
11
+ if (!isAdmin) {
12
+ const resourceConfig = resources?.[model];
13
+ const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("list");
14
+ if (!isPublic) {
15
+ throw createError({
16
+ statusCode: 401,
17
+ message: "Unauthorized"
18
+ });
19
+ }
20
+ }
6
21
  const table = getTableForModel(model);
7
- const records = await useDrizzle().select().from(table).all();
8
- return records;
22
+ const results = await useDrizzle().select().from(table).all();
23
+ return results.map((item) => {
24
+ if (isAdmin) {
25
+ return filterHiddenFields(model, item);
26
+ } else {
27
+ return filterPublicColumns(model, item);
28
+ }
29
+ });
9
30
  });
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, unknown>>>;
2
2
  export default _default;
@@ -1,14 +1,29 @@
1
- import { eventHandler, getRouterParams, readBody } from "h3";
2
- import { getTableForModel } from "../../utils/modelMapper.js";
1
+ import { eventHandler, getRouterParams, readBody, createError } from "h3";
2
+ import { getTableForModel, filterHiddenFields, filterUpdatableFields, filterPublicColumns } from "../../utils/modelMapper.js";
3
3
  import { useDrizzle } from "#site/drizzle";
4
+ import { useAutoCrudConfig } from "../../utils/config.js";
5
+ import { checkAdminAccess } from "../../utils/auth.js";
4
6
  export default eventHandler(async (event) => {
7
+ const { resources } = useAutoCrudConfig();
5
8
  const { model } = getRouterParams(event);
9
+ const isAdmin = await checkAdminAccess(event, model, "create");
10
+ if (!isAdmin) {
11
+ const resourceConfig = resources?.[model];
12
+ const isPublic = resourceConfig?.public === true || Array.isArray(resourceConfig?.public) && resourceConfig.public.includes("create");
13
+ if (!isPublic) {
14
+ throw createError({
15
+ statusCode: 401,
16
+ message: "Unauthorized"
17
+ });
18
+ }
19
+ }
6
20
  const table = getTableForModel(model);
7
21
  const body = await readBody(event);
8
- const values = {
9
- ...body,
10
- createdAt: /* @__PURE__ */ new Date()
11
- };
12
- const record = await useDrizzle().insert(table).values(values).returning().get();
13
- return record;
22
+ const payload = filterUpdatableFields(model, body);
23
+ const newRecord = await useDrizzle().insert(table).values(payload).returning().get();
24
+ if (isAdmin) {
25
+ return filterHiddenFields(model, newRecord);
26
+ } else {
27
+ return filterPublicColumns(model, newRecord);
28
+ }
14
29
  });
@@ -0,0 +1,2 @@
1
+ import type { H3Event } from 'h3';
2
+ export declare function checkAdminAccess(event: H3Event, model: string, action: string): Promise<boolean>;
@@ -0,0 +1,39 @@
1
+ import { createError } from "h3";
2
+ import { useAutoCrudConfig } from "./config.js";
3
+ import { verifyJwtToken } from "./jwt.js";
4
+ export async function checkAdminAccess(event, model, action) {
5
+ const { auth } = useAutoCrudConfig();
6
+ if (!auth?.enabled) {
7
+ return true;
8
+ }
9
+ if (auth.type === "jwt") {
10
+ if (!auth.jwtSecret) {
11
+ console.warn("JWT Secret is not configured but auth type is jwt");
12
+ return false;
13
+ }
14
+ return verifyJwtToken(event, auth.jwtSecret);
15
+ }
16
+ if (typeof requireUserSession !== "function") {
17
+ throw new TypeError("requireUserSession is not available");
18
+ }
19
+ try {
20
+ await requireUserSession(event);
21
+ if (auth.authorization) {
22
+ if (event.context.ability) {
23
+ const can = event.context.ability.can(action, model);
24
+ if (!can) {
25
+ throw createError({
26
+ statusCode: 403,
27
+ message: "Forbidden"
28
+ });
29
+ }
30
+ }
31
+ }
32
+ return true;
33
+ } catch (e) {
34
+ if (e.statusCode === 403) {
35
+ throw e;
36
+ }
37
+ return false;
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { ModuleOptions } from '../../../types.js';
2
+ export declare const useAutoCrudConfig: () => ModuleOptions;
@@ -0,0 +1,4 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ export const useAutoCrudConfig = () => {
3
+ return useRuntimeConfig().autoCrud;
4
+ };
@@ -0,0 +1,2 @@
1
+ import type { H3Event } from 'h3';
2
+ export declare function verifyJwtToken(event: H3Event, secret: string): Promise<boolean>;
@@ -0,0 +1,19 @@
1
+ import { jwtVerify } from "jose";
2
+ import { getRequestHeader } from "h3";
3
+ export async function verifyJwtToken(event, secret) {
4
+ const authHeader = getRequestHeader(event, "Authorization");
5
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
6
+ return false;
7
+ }
8
+ const token = authHeader.split(" ")[1];
9
+ if (!token) {
10
+ return false;
11
+ }
12
+ try {
13
+ const secretKey = new TextEncoder().encode(secret);
14
+ await jwtVerify(token, secretKey);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
@@ -9,6 +9,11 @@ import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
9
9
  * }
10
10
  */
11
11
  export declare const customUpdatableFields: Record<string, string[]>;
12
+ /**
13
+ * Custom hidden fields configuration (optional)
14
+ * Only define here if you want to override the default hidden fields
15
+ */
16
+ export declare const customHiddenFields: Record<string, string[]>;
12
17
  /**
13
18
  * Auto-generated model table map
14
19
  * Automatically includes all tables from schema
@@ -53,3 +58,29 @@ export declare function getModelPluralName(modelName: string): string;
53
58
  * @returns Array of model names
54
59
  */
55
60
  export declare function getAvailableModels(): string[];
61
+ /**
62
+ * Gets the hidden fields for a model
63
+ * @param modelName - The name of the model
64
+ * @returns Array of field names that should be hidden
65
+ */
66
+ export declare function getHiddenFields(modelName: string): string[];
67
+ /**
68
+ * Gets the public columns for a model
69
+ * @param modelName - The name of the model
70
+ * @returns Array of field names that are public (or undefined if all are public)
71
+ */
72
+ export declare function getPublicColumns(modelName: string): string[] | undefined;
73
+ /**
74
+ * Filters an object to only include public columns (if configured)
75
+ * @param modelName - The name of the model
76
+ * @param data - The data object to filter
77
+ * @returns Filtered object
78
+ */
79
+ export declare function filterPublicColumns(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
80
+ /**
81
+ * Filters an object to exclude hidden fields
82
+ * @param modelName - The name of the model
83
+ * @param data - The data object to filter
84
+ * @returns Filtered object without hidden fields
85
+ */
86
+ export declare function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
@@ -3,11 +3,16 @@ import pluralize from "pluralize";
3
3
  import { pascalCase } from "scule";
4
4
  import { getTableColumns as getDrizzleTableColumns } from "drizzle-orm";
5
5
  import { createError } from "h3";
6
+ import { useRuntimeConfig } from "#imports";
6
7
  const PROTECTED_FIELDS = ["id", "createdAt", "created_at"];
8
+ const HIDDEN_FIELDS = ["password", "secret", "token"];
7
9
  export const customUpdatableFields = {
8
10
  // Add custom field restrictions here if needed
9
11
  // By default, all fields except PROTECTED_FIELDS are updatable
10
12
  };
13
+ export const customHiddenFields = {
14
+ // Add custom hidden fields here if needed
15
+ };
11
16
  function buildModelTableMap() {
12
17
  const tableMap = {};
13
18
  for (const [key, value] of Object.entries(schema)) {
@@ -72,3 +77,36 @@ export function getModelPluralName(modelName) {
72
77
  export function getAvailableModels() {
73
78
  return Object.keys(modelTableMap);
74
79
  }
80
+ export function getHiddenFields(modelName) {
81
+ if (customHiddenFields[modelName]) {
82
+ return customHiddenFields[modelName];
83
+ }
84
+ return HIDDEN_FIELDS;
85
+ }
86
+ export function getPublicColumns(modelName) {
87
+ const { resources } = useRuntimeConfig().autoCrud;
88
+ return resources?.[modelName]?.publicColumns;
89
+ }
90
+ export function filterPublicColumns(modelName, data) {
91
+ const publicColumns = getPublicColumns(modelName);
92
+ if (!publicColumns) {
93
+ return filterHiddenFields(modelName, data);
94
+ }
95
+ const filtered = {};
96
+ for (const [key, value] of Object.entries(data)) {
97
+ if (publicColumns.includes(key) && !getHiddenFields(modelName).includes(key)) {
98
+ filtered[key] = value;
99
+ }
100
+ }
101
+ return filtered;
102
+ }
103
+ export function filterHiddenFields(modelName, data) {
104
+ const hiddenFields = getHiddenFields(modelName);
105
+ const filtered = {};
106
+ for (const [key, value] of Object.entries(data)) {
107
+ if (!hiddenFields.includes(key)) {
108
+ filtered[key] = value;
109
+ }
110
+ }
111
+ return filtered;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Exposes RESTful CRUD APIs for your Nuxt app based solely on your database migrations.",
5
5
  "author": "Cliford Pereira",
6
6
  "license": "MIT",
@@ -35,21 +35,24 @@
35
35
  ],
36
36
  "scripts": {
37
37
  "prepack": "nuxt-module-build build",
38
- "dev": "npm run dev:prepare && nuxi dev playground",
39
- "dev:build": "nuxi build playground",
40
- "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
38
+ "dev": "npm run dev:prepare && nuxi dev playground-fullstack",
39
+ "dev:build": "nuxi build playground-fullstack",
40
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground-fullstack",
41
41
  "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
42
42
  "lint": "eslint .",
43
43
  "test": "vitest run",
44
+ "test:api": "node scripts/test-api.mjs",
44
45
  "test:watch": "vitest watch",
45
- "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
46
+ "test:types": "vue-tsc --noEmit && cd playground-fullstack && vue-tsc --noEmit",
46
47
  "link": "npm link"
47
48
  },
48
49
  "dependencies": {
49
50
  "@nuxt/kit": "^4.2.1",
50
51
  "@types/pluralize": "^0.0.33",
51
52
  "pluralize": "^8.0.0",
52
- "scule": "^1.0.0"
53
+ "scule": "^1.0.0",
54
+ "c12": "^2.0.1",
55
+ "jose": "^5.9.6"
53
56
  },
54
57
  "peerDependencies": {
55
58
  "drizzle-orm": "^0.30.0"
@@ -60,12 +63,19 @@
60
63
  "@nuxt/module-builder": "^1.0.2",
61
64
  "@nuxt/schema": "^4.2.1",
62
65
  "@nuxt/test-utils": "^3.20.1",
66
+ "@nuxthub/core": "^0.9.1",
63
67
  "@types/node": "latest",
68
+ "better-sqlite3": "^12.4.6",
64
69
  "changelogen": "^0.6.2",
70
+ "drizzle-kit": "^0.31.7",
71
+ "drizzle-orm": "^0.38.3",
65
72
  "eslint": "^9.39.1",
66
73
  "nuxt": "^4.2.1",
74
+ "nuxt-auth-utils": "^0.5.25",
75
+ "nuxt-authorization": "^0.3.5",
67
76
  "typescript": "~5.9.3",
68
77
  "vitest": "^4.0.13",
69
- "vue-tsc": "^3.1.5"
78
+ "vue-tsc": "^3.1.5",
79
+ "wrangler": "^4.51.0"
70
80
  }
71
81
  }
@@ -1,15 +1,36 @@
1
1
  // server/api/[model]/[id].delete.ts
2
2
  import { eventHandler, getRouterParams, createError } from 'h3'
3
3
  import { eq } from 'drizzle-orm'
4
- import { getTableForModel, getModelSingularName } from '../../utils/modelMapper'
4
+ import { getTableForModel, getModelSingularName, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
5
5
 
6
6
  import type { TableWithId } from '../../types'
7
7
  // @ts-expect-error - #site/drizzle is an alias defined by the module
8
8
  import { useDrizzle } from '#site/drizzle'
9
9
 
10
+ import { useAutoCrudConfig } from '../../utils/config'
11
+ import { checkAdminAccess } from '../../utils/auth'
12
+
10
13
  export default eventHandler(async (event) => {
11
- const { model, id } = getRouterParams(event)
14
+ const { resources } = useAutoCrudConfig()
15
+ const { model, id } = getRouterParams(event) as { model: string, id: string }
16
+
17
+ const isAdmin = await checkAdminAccess(event, model, 'delete')
18
+
19
+ // Check public access if not admin
20
+ if (!isAdmin) {
21
+ const resourceConfig = resources?.[model]
22
+ const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('delete'))
23
+
24
+ if (!isPublic) {
25
+ throw createError({
26
+ statusCode: 401,
27
+ message: 'Unauthorized',
28
+ })
29
+ }
30
+ }
31
+
12
32
  const table = getTableForModel(model) as TableWithId
33
+
13
34
  const singularName = getModelSingularName(model)
14
35
 
15
36
  const deletedRecord = await useDrizzle()
@@ -25,5 +46,10 @@ export default eventHandler(async (event) => {
25
46
  })
26
47
  }
27
48
 
28
- return deletedRecord
49
+ if (isAdmin) {
50
+ return filterHiddenFields(model, deletedRecord as Record<string, unknown>)
51
+ }
52
+ else {
53
+ return filterPublicColumns(model, deletedRecord as Record<string, unknown>)
54
+ }
29
55
  })
@@ -1,16 +1,35 @@
1
1
  // server/api/[model]/[id].get.ts
2
2
  import { eventHandler, getRouterParams, createError } from 'h3'
3
3
  import { eq } from 'drizzle-orm'
4
- import { getTableForModel, getModelSingularName } from '../../utils/modelMapper'
4
+ import { getTableForModel, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
5
5
 
6
6
  import type { TableWithId } from '../../types'
7
7
  // @ts-expect-error - #site/drizzle is an alias defined by the module
8
8
  import { useDrizzle } from '#site/drizzle'
9
9
 
10
+ import { useAutoCrudConfig } from '../../utils/config'
11
+ import { checkAdminAccess } from '../../utils/auth'
12
+
10
13
  export default eventHandler(async (event) => {
11
- const { model, id } = getRouterParams(event)
14
+ const { resources } = useAutoCrudConfig()
15
+ const { model, id } = getRouterParams(event) as { model: string, id: string }
16
+
17
+ const isAdmin = await checkAdminAccess(event, model, 'read')
18
+
19
+ // Check public access if not admin
20
+ if (!isAdmin) {
21
+ const resourceConfig = resources?.[model]
22
+ const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('read'))
23
+
24
+ if (!isPublic) {
25
+ throw createError({
26
+ statusCode: 401,
27
+ message: 'Unauthorized',
28
+ })
29
+ }
30
+ }
31
+
12
32
  const table = getTableForModel(model) as TableWithId
13
- const singularName = getModelSingularName(model)
14
33
 
15
34
  const record = await useDrizzle()
16
35
  .select()
@@ -21,9 +40,14 @@ export default eventHandler(async (event) => {
21
40
  if (!record) {
22
41
  throw createError({
23
42
  statusCode: 404,
24
- message: `${singularName} not found`,
43
+ message: 'Record not found',
25
44
  })
26
45
  }
27
46
 
28
- return record
47
+ if (isAdmin) {
48
+ return filterHiddenFields(model, record as Record<string, unknown>)
49
+ }
50
+ else {
51
+ return filterPublicColumns(model, record as Record<string, unknown>)
52
+ }
29
53
  })
@@ -1,26 +1,57 @@
1
1
  // server/api/[model]/[id].patch.ts
2
- import { eventHandler, getRouterParams, readBody } from 'h3'
2
+ import { eventHandler, getRouterParams, readBody, createError } from 'h3'
3
3
  import { eq } from 'drizzle-orm'
4
- import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
4
+ import { getTableForModel, filterUpdatableFields, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
5
5
 
6
6
  import type { TableWithId } from '../../types'
7
7
  // @ts-expect-error - #site/drizzle is an alias defined by the module
8
8
  import { useDrizzle } from '#site/drizzle'
9
9
 
10
+ import { useAutoCrudConfig } from '../../utils/config'
11
+ import { checkAdminAccess } from '../../utils/auth'
12
+
10
13
  export default eventHandler(async (event) => {
11
- const { model, id } = getRouterParams(event)
14
+ const { resources } = useAutoCrudConfig()
15
+ const { model, id } = getRouterParams(event) as { model: string, id: string }
16
+
17
+ const isAdmin = await checkAdminAccess(event, model, 'update')
18
+
19
+ // Check public access if not admin
20
+ if (!isAdmin) {
21
+ const resourceConfig = resources?.[model]
22
+ const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('update'))
23
+
24
+ if (!isPublic) {
25
+ throw createError({
26
+ statusCode: 401,
27
+ message: 'Unauthorized',
28
+ })
29
+ }
30
+ }
31
+
12
32
  const table = getTableForModel(model) as TableWithId
13
- const body = await readBody(event)
14
33
 
15
- // Filter to only allow updatable fields for this model
16
- const updateData = filterUpdatableFields(model, body)
34
+ const body = await readBody(event)
35
+ const payload = filterUpdatableFields(model, body)
17
36
 
18
- const record = await useDrizzle()
37
+ const updatedRecord = await useDrizzle()
19
38
  .update(table)
20
- .set(updateData)
39
+ .set(payload)
21
40
  .where(eq(table.id, Number(id)))
22
41
  .returning()
23
42
  .get()
24
43
 
25
- return record
44
+ if (!updatedRecord) {
45
+ throw createError({
46
+ statusCode: 404,
47
+ message: 'Record not found',
48
+ })
49
+ }
50
+
51
+ if (isAdmin) {
52
+ return filterHiddenFields(model, updatedRecord as Record<string, unknown>)
53
+ }
54
+ else {
55
+ return filterPublicColumns(model, updatedRecord as Record<string, unknown>)
56
+ }
26
57
  })
@@ -1,15 +1,43 @@
1
1
  // server/api/[model]/index.get.ts
2
- import { eventHandler, getRouterParams } from 'h3'
3
- import { getTableForModel } from '../../utils/modelMapper'
2
+ import { eventHandler, getRouterParams, createError } from 'h3'
3
+ import { getTableForModel, filterHiddenFields, filterPublicColumns } from '../../utils/modelMapper'
4
4
 
5
5
  // @ts-expect-error - #site/drizzle is an alias defined by the module
6
6
  import { useDrizzle } from '#site/drizzle'
7
7
 
8
+ import { useAutoCrudConfig } from '../../utils/config'
9
+ import { checkAdminAccess } from '../../utils/auth'
10
+
8
11
  export default eventHandler(async (event) => {
9
- const { model } = getRouterParams(event)
12
+ console.log('[GET] Request received', event.path)
13
+ const { resources } = useAutoCrudConfig()
14
+ const { model } = getRouterParams(event) as { model: string }
15
+
16
+ const isAdmin = await checkAdminAccess(event, model, 'list')
17
+
18
+ // Check public access if not admin
19
+ if (!isAdmin) {
20
+ const resourceConfig = resources?.[model]
21
+ const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('list'))
22
+
23
+ if (!isPublic) {
24
+ throw createError({
25
+ statusCode: 401,
26
+ message: 'Unauthorized',
27
+ })
28
+ }
29
+ }
30
+
10
31
  const table = getTableForModel(model)
11
32
 
12
- const records = await useDrizzle().select().from(table).all()
33
+ const results = await useDrizzle().select().from(table).all()
13
34
 
14
- return records
35
+ return results.map((item: Record<string, unknown>) => {
36
+ if (isAdmin) {
37
+ return filterHiddenFields(model, item as Record<string, unknown>)
38
+ }
39
+ else {
40
+ return filterPublicColumns(model, item as Record<string, unknown>)
41
+ }
42
+ })
15
43
  })
@@ -1,26 +1,43 @@
1
1
  // server/api/[model]/index.post.ts
2
- import { eventHandler, getRouterParams, readBody } from 'h3'
3
- import { getTableForModel } from '../../utils/modelMapper'
2
+ import { eventHandler, getRouterParams, readBody, createError } from 'h3'
3
+ import { getTableForModel, filterHiddenFields, filterUpdatableFields, filterPublicColumns } from '../../utils/modelMapper'
4
4
 
5
5
  // @ts-expect-error - #site/drizzle is an alias defined by the module
6
6
  import { useDrizzle } from '#site/drizzle'
7
7
 
8
+ import { useAutoCrudConfig } from '../../utils/config'
9
+ import { checkAdminAccess } from '../../utils/auth'
10
+
8
11
  export default eventHandler(async (event) => {
9
- const { model } = getRouterParams(event)
10
- const table = getTableForModel(model)
11
- const body = await readBody(event)
12
+ const { resources } = useAutoCrudConfig()
13
+ const { model } = getRouterParams(event) as { model: string }
12
14
 
13
- // Add createdAt timestamp
14
- const values = {
15
- ...body,
16
- createdAt: new Date(),
15
+ const isAdmin = await checkAdminAccess(event, model, 'create')
16
+
17
+ // Check public access if not admin
18
+ if (!isAdmin) {
19
+ const resourceConfig = resources?.[model]
20
+ const isPublic = resourceConfig?.public === true || (Array.isArray(resourceConfig?.public) && resourceConfig.public.includes('create'))
21
+
22
+ if (!isPublic) {
23
+ throw createError({
24
+ statusCode: 401,
25
+ message: 'Unauthorized',
26
+ })
27
+ }
17
28
  }
18
29
 
19
- const record = await useDrizzle()
20
- .insert(table)
21
- .values(values)
22
- .returning()
23
- .get()
30
+ const table = getTableForModel(model)
24
31
 
25
- return record
32
+ const body = await readBody(event)
33
+ const payload = filterUpdatableFields(model, body)
34
+
35
+ const newRecord = await useDrizzle().insert(table).values(payload).returning().get()
36
+
37
+ if (isAdmin) {
38
+ return filterHiddenFields(model, newRecord as Record<string, unknown>)
39
+ }
40
+ else {
41
+ return filterPublicColumns(model, newRecord as Record<string, unknown>)
42
+ }
26
43
  })
@@ -0,0 +1,55 @@
1
+ import type { H3Event } from 'h3'
2
+ import { createError } from 'h3'
3
+ import { useAutoCrudConfig } from './config'
4
+ import { verifyJwtToken } from './jwt'
5
+
6
+ export async function checkAdminAccess(event: H3Event, model: string, action: string): Promise<boolean> {
7
+ const { auth } = useAutoCrudConfig()
8
+
9
+ if (!auth?.enabled) {
10
+ return true
11
+ }
12
+
13
+ if (auth.type === 'jwt') {
14
+ if (!auth.jwtSecret) {
15
+ console.warn('JWT Secret is not configured but auth type is jwt')
16
+ return false
17
+ }
18
+ return verifyJwtToken(event, auth.jwtSecret)
19
+ }
20
+
21
+ // 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
+ try {
28
+ // @ts-expect-error - requireUserSession is auto-imported
29
+ await requireUserSession(event)
30
+
31
+ // Check authorization if enabled
32
+ if (auth.authorization) {
33
+ if (event.context.ability) {
34
+ const can = event.context.ability.can(action, model)
35
+ if (!can) {
36
+ throw createError({
37
+ statusCode: 403,
38
+ message: 'Forbidden',
39
+ })
40
+ }
41
+ }
42
+ }
43
+
44
+ return true
45
+ }
46
+ catch (e: unknown) {
47
+ // If it's a 403 (Forbidden) from our ability check, rethrow it
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ if ((e as any).statusCode === 403) {
50
+ throw e
51
+ }
52
+ // Otherwise (401 from requireUserSession), return false (treat as guest)
53
+ return false
54
+ }
55
+ }
@@ -0,0 +1,6 @@
1
+ import { useRuntimeConfig } from '#imports'
2
+ import type { ModuleOptions } from '../../../types'
3
+
4
+ export const useAutoCrudConfig = (): ModuleOptions => {
5
+ return useRuntimeConfig().autoCrud as ModuleOptions
6
+ }
@@ -0,0 +1,23 @@
1
+ import { jwtVerify } from 'jose'
2
+ import type { H3Event } from 'h3'
3
+ import { getRequestHeader } from 'h3'
4
+
5
+ export async function verifyJwtToken(event: H3Event, secret: string): Promise<boolean> {
6
+ const authHeader = getRequestHeader(event, 'Authorization')
7
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
8
+ return false
9
+ }
10
+
11
+ const token = authHeader.split(' ')[1]
12
+ if (!token) {
13
+ return false
14
+ }
15
+ try {
16
+ const secretKey = new TextEncoder().encode(secret)
17
+ await jwtVerify(token, secretKey)
18
+ return true
19
+ }
20
+ catch {
21
+ return false
22
+ }
23
+ }
@@ -6,12 +6,18 @@ import { pascalCase } from 'scule'
6
6
  import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
7
7
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
8
8
  import { createError } from 'h3'
9
+ import { useRuntimeConfig } from '#imports'
9
10
 
10
11
  /**
11
12
  * Fields that should never be updatable via PATCH requests
12
13
  */
13
14
  const PROTECTED_FIELDS = ['id', 'createdAt', 'created_at']
14
15
 
16
+ /**
17
+ * Fields that should never be returned in API responses
18
+ */
19
+ const HIDDEN_FIELDS = ['password', 'secret', 'token']
20
+
15
21
  /**
16
22
  * Custom updatable fields configuration (optional)
17
23
  * Only define here if you want to override the auto-detection
@@ -26,6 +32,14 @@ export const customUpdatableFields: Record<string, string[]> = {
26
32
  // By default, all fields except PROTECTED_FIELDS are updatable
27
33
  }
28
34
 
35
+ /**
36
+ * Custom hidden fields configuration (optional)
37
+ * Only define here if you want to override the default hidden fields
38
+ */
39
+ export const customHiddenFields: Record<string, string[]> = {
40
+ // Add custom hidden fields here if needed
41
+ }
42
+
29
43
  /**
30
44
  * Automatically builds a map of all exported tables from the schema
31
45
  * No manual configuration needed!
@@ -163,3 +177,72 @@ export function getModelPluralName(modelName: string): string {
163
177
  export function getAvailableModels(): string[] {
164
178
  return Object.keys(modelTableMap)
165
179
  }
180
+
181
+ /**
182
+ * Gets the hidden fields for a model
183
+ * @param modelName - The name of the model
184
+ * @returns Array of field names that should be hidden
185
+ */
186
+ export function getHiddenFields(modelName: string): string[] {
187
+ // Check if custom hidden fields are defined for this model
188
+ if (customHiddenFields[modelName]) {
189
+ return customHiddenFields[modelName]
190
+ }
191
+
192
+ return HIDDEN_FIELDS
193
+ }
194
+
195
+ /**
196
+ * Gets the public columns for a model
197
+ * @param modelName - The name of the model
198
+ * @returns Array of field names that are public (or undefined if all are public)
199
+ */
200
+ export function getPublicColumns(modelName: string): string[] | undefined {
201
+ const { resources } = useRuntimeConfig().autoCrud
202
+ return resources?.[modelName]?.publicColumns
203
+ }
204
+
205
+ /**
206
+ * Filters an object to only include public columns (if configured)
207
+ * @param modelName - The name of the model
208
+ * @param data - The data object to filter
209
+ * @returns Filtered object
210
+ */
211
+ export function filterPublicColumns(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
212
+ const publicColumns = getPublicColumns(modelName)
213
+
214
+ // If no public columns configured, return all (except hidden)
215
+ if (!publicColumns) {
216
+ return filterHiddenFields(modelName, data)
217
+ }
218
+
219
+ const filtered: Record<string, unknown> = {}
220
+
221
+ for (const [key, value] of Object.entries(data)) {
222
+ // Must be in publicColumns AND not in hidden fields (double safety)
223
+ if (publicColumns.includes(key) && !getHiddenFields(modelName).includes(key)) {
224
+ filtered[key] = value
225
+ }
226
+ }
227
+
228
+ return filtered
229
+ }
230
+
231
+ /**
232
+ * Filters an object to exclude hidden fields
233
+ * @param modelName - The name of the model
234
+ * @param data - The data object to filter
235
+ * @returns Filtered object without hidden fields
236
+ */
237
+ export function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown> {
238
+ const hiddenFields = getHiddenFields(modelName)
239
+ const filtered: Record<string, unknown> = {}
240
+
241
+ for (const [key, value] of Object.entries(data)) {
242
+ if (!hiddenFields.includes(key)) {
243
+ filtered[key] = value
244
+ }
245
+ }
246
+
247
+ return filtered
248
+ }