nuxt-auto-crud 2.3.0 → 2.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.
package/README.md CHANGED
@@ -108,9 +108,9 @@ nuxt dev
108
108
 
109
109
  ---
110
110
 
111
- ## 🌐 Exposed Dynamic RESTful CRUD endpoints
111
+ ## 🌐 Data APIs (Dynamic RESTful CRUD)
112
112
 
113
- Nb: Endpoints follow the pattern `/api/_nac/:model`.
113
+ > Note: All endpoints follow the pattern ${nacEndpointPrefix}/:model. By default, this is /api/_nac/:model.
114
114
 
115
115
  | Method | Endpoint | Action |
116
116
  | --- | --- | --- |
@@ -133,15 +133,23 @@ Nb: Endpoints follow the pattern `/api/_nac/:model`.
133
133
 
134
134
  ---
135
135
 
136
- ## 🛠 Frontend Integration APIs
136
+ ## 🛠 Introspection & Metadata APIs
137
137
 
138
- In addition to CRUD endpoints, **nac** provides metadata APIs to power dynamic forms and tables in your frontend.
138
+ Use these endpoints to build dynamic UI components (like menus and forms) or provide context to AI agents. These use the `_schemas` and `_meta` reserved paths.
139
139
 
140
- * **List Resources**: `GET /api/_nac/_schemas` returns all tables (excluding system-protected tables).
141
- * **Resource Metadata**: `GET /api/_nac/_schemas/:resource` returns the field definitions, validation rules, and relationship data for a specific table.
142
- * **Agentic Discovery**: `GET /api/_nac/_meta?format=md` returns a markdown manifest for LLM context injection.
140
+ ### 1. Discovery Endpoints
143
141
 
144
- ---
142
+ * **List Resource Names**: `GET /api/_nac/_schemas`
143
+ * Returns an array of all available table names. Useful for generating dynamic navigation menus.
144
+ * **Resource Metadata**: `GET /api/_nac/_schemas/:resource`
145
+ * Returns field definitions, validation rules, and `isReadOnly` status for a specific table.
146
+ * **Example:** `GET /api/_nac/_schemas/users` returns the schema for the users table.
147
+
148
+ ### 2. Agentic Discovery
149
+
150
+ * **Manifest**: `GET /api/_nac/_meta?format=md`
151
+ * Returns a token-efficient Markdown manifest for LLM context injection.
152
+ * **Security:** Requires `NUXT_AUTO_CRUD_AGENTIC_TOKEN` (min 16 characters) in your `.env`.
145
153
 
146
154
  ### Schema Interface
147
155
 
@@ -187,8 +195,9 @@ Enabling `authentication` in the `autoCrud` config protects all **nac** routes (
187
195
 
188
196
  ### 🔒 Access Control & Data Safety
189
197
 
190
- * **`apiHiddenFields`**: Globally hides sensitive columns from all API responses. Default: `['password', 'secret', 'token', 'reset_token', 'reset_expires', 'github_id', 'google_id']`.
191
- * **`formHiddenFields`**: Columns excluded from the frontend schema metadata to prevent user input. Defaults to `apiHiddenFields` plus system-managed fields like `id`, `uuid`, `createdAt`, `updatedAt`, `createdBy`, etc.
198
+ * **`apiHiddenFields`**: Globally hides sensitive columns from all API responses. Default: ['password', 'secret', 'token', 'resetToken', 'resetExpires', 'githubId', 'googleId'].
199
+ * **`formHiddenFields`**: Columns excluded from the frontend schema metadata to prevent user input. Defaults to apiHiddenFields plus system-managed fields like `id`, `uuid`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, and `updatedBy`.
200
+ * **`formReadOnlyFields`**: Columns visible in the UI for context but protected from user modification (e.g., slug, status).
192
201
  * **Response Scrubbing**: If a field is in `apiHiddenFields` or does not exist in the schema, it is silently stripped from the response even if listed in `publicResources`.
193
202
 
194
203
  ---
@@ -198,11 +207,15 @@ Enabling `authentication` in the `autoCrud` config protects all **nac** routes (
198
207
  | Key | Default | Description |
199
208
  | --- | --- | --- |
200
209
  | `statusFiltering` | `false` | Enables/disables automatic filtering of records based on the `status` column. |
201
- | `realtime` | `false` | Enables/disables real-time capabilities. |
210
+ | `realtime` | `false` | Enables real-time broadcasting of all Create, Update, and Delete (CUD) operations via SSE. |
202
211
  | `auth.authentication` | `false` | Requires a valid session for all NAC routes. |
203
212
  | `auth.authorization` | `false` | Enables role/owner-based access checks. |
204
213
  | `auth.ownerKey` | `'createdBy'` | The column name used to identify the record creator. |
205
214
  | `publicResources` | `{}` | Defines tables and specific columns accessible without auth. |
215
+ | `apiHiddenFields` | `NAC_API_HIDDEN_FIELDS` | Arrays of keys to exclude from all API responses. |
216
+ | `formHiddenFields` | `NAC_FORM_HIDDEN_FIELDS` | Arrays of keys to exclude from dynamic forms. |
217
+ | `formReadOnlyFields` | `NAC_FORM_READ_ONLY_FIELDS` | List of visible but non-editable fields (UI only). |
218
+ | `agenticToken` | `''` | Secret key used to secure the /_meta endpoint, preventing unauthorized AI agents from introspecting your schema. |
206
219
  | `nacEndpointPrefix` | `'/api/_nac'` | The base path for NAC routes. Access via `useRuntimeConfig().public.autoCrud`. |
207
220
  | `schemaPath` | `'server/db/schema'` | Location of your Drizzle schema files. |
208
221
 
@@ -221,8 +234,9 @@ autoCrud: {
221
234
  users: ['id', 'name', 'email'],
222
235
  },
223
236
  apiHiddenFields: ['password'],
224
- agenticToken: process.env.NAC_AGENTIC_TOKEN,
225
- formHiddenFields: [],
237
+ formHiddenFields: ['createdAt'], // All fields should be camelCase
238
+ formReadOnlyFields: ['slug', 'externalId'], // Locked for user input
239
+ agenticToken: '',
226
240
  nacEndpointPrefix: '/api/_nac',
227
241
  schemaPath: 'server/db/schema',
228
242
  }
@@ -253,7 +267,7 @@ If `list_active` is present, it applies a hybrid OR logic: users can see all act
253
267
  // Example: Setting context in your Auth Middleware
254
268
  event.context.nac = {
255
269
  userId: user.id,
256
- resourcePermissions: user.permissions[model], // e.g., ['list_own', 'list']
270
+ resourcePermissions: user.permissions[model], // e.g., ['list_own', 'list_active']
257
271
  record: null, // Optional: Pre-fetched record to prevent double-hitting the DB
258
272
  }
259
273
 
@@ -306,6 +320,3 @@ useNacAutoCrudSSE(({ table, action, data: sseData, primaryKey }) => {
306
320
  ```
307
321
  ---
308
322
 
309
-
310
-
311
- ---
package/dist/module.d.mts CHANGED
@@ -14,13 +14,14 @@ interface ModuleOptions {
14
14
  publicResources: Record<string, string[]>; /** Allowed fields for public apis */
15
15
  nacEndpointPrefix: string;
16
16
  formHiddenFields: string[]; /** UI: Hidden from forms */
17
+ formReadOnlyFields: string[]; /** UI: Read only fields */
17
18
  }
18
19
  declare module '@nuxt/schema' {
19
20
  interface RuntimeConfig {
20
- autoCrud: Omit<ModuleOptions, 'nacEndpointPrefix' | 'formHiddenFields'>;
21
+ autoCrud: Omit<ModuleOptions, 'nacEndpointPrefix' | 'formHiddenFields' | 'formReadOnlyFields'>;
21
22
  }
22
23
  interface PublicRuntimeConfig {
23
- autoCrud: Pick<ModuleOptions, 'nacEndpointPrefix' | 'formHiddenFields'>;
24
+ autoCrud: Pick<ModuleOptions, 'nacEndpointPrefix' | 'formHiddenFields' | 'formReadOnlyFields'>;
24
25
  }
25
26
  }
26
27
 
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "2.3.0",
4
+ "version": "2.4.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineNuxtModule, createResolver, addImportsDir, addServerImportsDir, addServerHandler } from '@nuxt/kit';
2
- import { NAC_FORM_HIDDEN_FIELDS, NAC_API_HIDDEN_FIELDS } from '../dist/runtime/server/utils/constants.js';
2
+ import { NAC_FORM_READ_ONLY_FIELDS, NAC_FORM_HIDDEN_FIELDS, NAC_API_HIDDEN_FIELDS } from '../dist/runtime/server/utils/constants.js';
3
3
 
4
4
  const module$1 = defineNuxtModule({
5
5
  meta: {
@@ -21,6 +21,7 @@ const module$1 = defineNuxtModule({
21
21
  schemaPath: "server/db/schema",
22
22
  // Public config
23
23
  formHiddenFields: NAC_FORM_HIDDEN_FIELDS,
24
+ formReadOnlyFields: NAC_FORM_READ_ONLY_FIELDS,
24
25
  nacEndpointPrefix: "/api/_nac"
25
26
  },
26
27
  async setup(options, nuxt) {
@@ -29,9 +30,9 @@ const module$1 = defineNuxtModule({
29
30
  nuxt.options.alias["#nac/shared"] = resolver.resolve("./runtime/shared");
30
31
  nuxt.options.alias["#nac/types"] = resolver.resolve("./runtime/server/types");
31
32
  nuxt.options.alias["#nac/schema"] = resolver.resolve(nuxt.options.rootDir, options.schemaPath);
32
- const { formHiddenFields, nacEndpointPrefix, ...privateOptions } = options;
33
+ const { formHiddenFields, nacEndpointPrefix, formReadOnlyFields, ...privateOptions } = options;
33
34
  nuxt.options.runtimeConfig.autoCrud = privateOptions;
34
- nuxt.options.runtimeConfig.public.autoCrud = { formHiddenFields, nacEndpointPrefix };
35
+ nuxt.options.runtimeConfig.public.autoCrud = { formHiddenFields, nacEndpointPrefix, formReadOnlyFields };
35
36
  addImportsDir(resolver.resolve("./runtime/composables"));
36
37
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
37
38
  nuxt.hook("prepare:types", ({ references }) => {
@@ -1,5 +1,5 @@
1
1
  import { db } from "@nuxthub/db";
2
- import { eventHandler, getQuery, getHeader } from "h3";
2
+ import { eventHandler, getQuery, getHeader, setResponseHeader } from "h3";
3
3
  import { useRuntimeConfig } from "#imports";
4
4
  import { getSchemaDefinition, modelTableMap } from "../../utils/modelMapper.js";
5
5
  export default eventHandler(async (event) => {
@@ -12,7 +12,7 @@ export default defineEventHandler(async (event) => {
12
12
  const isUserAuthenticated = Boolean(event.context.nac?.userId);
13
13
  if (isAuthEnabled && !isUserAuthenticated) {
14
14
  const model = getModelName(pathname, nacEndpointPrefix);
15
- if (model && isPublicResource(model)) {
15
+ if (model && isPublicResource(model, config.autoCrud.publicResources)) {
16
16
  event.context.nac.isPublic = true;
17
17
  } else {
18
18
  throw new AuthenticationError("Unauthorized").toH3();
@@ -22,18 +22,26 @@ export default defineEventHandler(async (event) => {
22
22
  }
23
23
  const token = getQuery(event).token;
24
24
  const { agenticToken } = config.autoCrud;
25
- if (!agenticToken || token !== agenticToken) {
25
+ if (!validateToken(token, agenticToken)) {
26
26
  throw new AuthenticationError("Invalid agentic token").toH3();
27
27
  }
28
28
  });
29
+ function validateToken(token, agenticToken) {
30
+ if (!token || !agenticToken || agenticToken.length < 16) return false;
31
+ if (token.length !== agenticToken.length) return false;
32
+ let diff = 0;
33
+ for (let i = 0; i < token.length; i++) {
34
+ diff |= token.charCodeAt(i) ^ agenticToken.charCodeAt(i);
35
+ }
36
+ return diff === 0;
37
+ }
29
38
  function getModelName(pathname, nacEndpointPrefix) {
30
39
  const regex = new RegExp(`^${nacEndpointPrefix}/([^/]+)`);
31
40
  const match = pathname.match(regex);
32
41
  return match ? match[1] : null;
33
42
  }
34
- function isPublicResource(model) {
35
- const { publicResources } = useRuntimeConfig().autoCrud;
36
- return Object.keys(publicResources || {}).includes(model);
43
+ function isPublicResource(model, publicResources = {}) {
44
+ return Object.keys(publicResources).includes(model);
37
45
  }
38
46
  function isAgenticPath(pathname) {
39
47
  return pathname.includes("/_meta");
@@ -5,14 +5,19 @@
5
5
  export declare const NAC_API_HIDDEN_FIELDS: string[];
6
6
  /**
7
7
  * 2. FORM_HIDDEN_FIELDS
8
- * User should not edit these. System handles these.
9
- * Includes System Fields (minus status/updated_at) + Hidden Fields.
10
8
  */
11
9
  export declare const NAC_FORM_HIDDEN_FIELDS: string[];
12
10
  /**
13
- * 3. TABLE_HIDDEN_FIELDS
14
- * UI clutter reduction for DataTables.
11
+ * 3. DATA_TABLE_HIDDEN_FIELDS
15
12
  */
16
13
  export declare const NAC_DATA_TABLE_HIDDEN_FIELDS: string[];
17
- /** Tables used by the underlying database engine/migration tool */
14
+ /**
15
+ * 4. FORM_READ_ONLY_FIELDS
16
+ * Visible in forms for context, but not editable.
17
+ */
18
+ export declare const NAC_FORM_READ_ONLY_FIELDS: never[];
19
+ /**
20
+ * Tables used by the engine.
21
+ * These match the actual DB table names (usually snake_case or specific migration names).
22
+ */
18
23
  export declare const NAC_SYSTEM_TABLES: string[];
@@ -1,32 +1,27 @@
1
- const NAC_SYSTEM_FIELDS = [
2
- "id",
3
- "uuid",
4
- "created_at",
5
- "updated_at",
6
- "deleted_at",
7
- "created_by",
8
- "updated_by"
9
- ];
10
1
  export const NAC_API_HIDDEN_FIELDS = [
11
2
  "password",
12
3
  "secret",
13
4
  "token",
14
- "reset_token",
15
- "reset_expires",
16
- "github_id",
17
- "google_id"
5
+ "resetToken",
6
+ "resetExpires",
7
+ "githubId",
8
+ "googleId"
18
9
  ];
19
10
  export const NAC_FORM_HIDDEN_FIELDS = [
20
- // ...NAC_API_HIDDEN_FIELDS, // hidden by default.
21
- ...NAC_SYSTEM_FIELDS
22
- // from the api, hide system fields too
11
+ ...NAC_API_HIDDEN_FIELDS,
12
+ "id",
13
+ "uuid",
14
+ "createdAt",
15
+ "updatedAt",
16
+ "deletedAt",
17
+ "createdBy",
18
+ "updatedBy"
23
19
  ];
24
20
  export const NAC_DATA_TABLE_HIDDEN_FIELDS = [
25
- // ...NAC_API_HIDDEN_FIELDS, // hidden by default.
26
- "updated_at",
27
- "deleted_at",
28
- "created_by",
29
- "updated_by"
30
- // from the api, hide these fields too
21
+ "updatedAt",
22
+ "deletedAt",
23
+ "createdBy",
24
+ "updatedBy"
31
25
  ];
26
+ export const NAC_FORM_READ_ONLY_FIELDS = [];
32
27
  export const NAC_SYSTEM_TABLES = ["_hub_migrations", "d1_migrations", "sqlite_sequence"];
@@ -76,11 +76,11 @@ export async function getSchemaDefinition(modelName) {
76
76
  if (!table) throw new ResourceNotFoundError(modelName);
77
77
  const config = useRuntimeConfig();
78
78
  const apiHiddenFields = config.autoCrud.apiHiddenFields;
79
- const formHiddenFields = config.public.autoCrud.formHiddenFields;
79
+ const { formHiddenFields, formReadOnlyFields } = config.public.autoCrud;
80
80
  const columns = getColumns(table);
81
81
  const relations = await resolveTableRelations(table);
82
82
  const shape = createInsertSchema(table).shape;
83
- const fields = Object.entries(columns).filter(([name]) => !apiHiddenFields.includes(name)).map(([name, col]) => {
83
+ const fields = Object.entries(columns).filter(([name]) => !apiHiddenFields.includes(name) && !formHiddenFields.includes(name)).map(([name, col]) => {
84
84
  const zodField = shape[name];
85
85
  const zodTypeName = zodField?._def?.typeName;
86
86
  let type = ZOD_TYPE_MAP[zodTypeName] ?? "string";
@@ -105,7 +105,7 @@ export async function getSchemaDefinition(modelName) {
105
105
  selectOptions,
106
106
  required: colInternal.notNull ?? false,
107
107
  references: relations[name],
108
- isReadOnly: formHiddenFields.includes(name)
108
+ isReadOnly: formReadOnlyFields.includes(name) || name === "id"
109
109
  };
110
110
  });
111
111
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Dynamic RESTful CRUD APIs for Nuxt without code generation, fully schema-driven.",
5
5
  "author": "Cliford Pereira",
6
6
  "license": "MIT",