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 +28 -17
- package/dist/module.d.mts +3 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +4 -3
- package/dist/runtime/server/api/_nac/_meta.get.js +1 -1
- package/dist/runtime/server/middleware/nac-guard.js +13 -5
- package/dist/runtime/server/utils/constants.d.ts +10 -5
- package/dist/runtime/server/utils/constants.js +17 -22
- package/dist/runtime/server/utils/modelMapper.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,9 +108,9 @@ nuxt dev
|
|
|
108
108
|
|
|
109
109
|
---
|
|
110
110
|
|
|
111
|
-
## 🌐
|
|
111
|
+
## 🌐 Data APIs (Dynamic RESTful CRUD)
|
|
112
112
|
|
|
113
|
-
|
|
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
|
-
## 🛠
|
|
136
|
+
## 🛠 Introspection & Metadata APIs
|
|
137
137
|
|
|
138
|
-
|
|
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
|
-
|
|
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:
|
|
191
|
-
* **`formHiddenFields`**: Columns excluded from the frontend schema metadata to prevent user input. Defaults to
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
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', '
|
|
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
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 (!
|
|
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
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
5
|
+
"resetToken",
|
|
6
|
+
"resetExpires",
|
|
7
|
+
"githubId",
|
|
8
|
+
"googleId"
|
|
18
9
|
];
|
|
19
10
|
export const NAC_FORM_HIDDEN_FIELDS = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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
|
|
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:
|
|
108
|
+
isReadOnly: formReadOnlyFields.includes(name) || name === "id"
|
|
109
109
|
};
|
|
110
110
|
});
|
|
111
111
|
return {
|