nuxt-auto-crud 1.24.0 → 1.26.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.
@@ -1,86 +1,28 @@
1
1
  import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
2
- /**
3
- * Custom updatable fields configuration (optional)
4
- * Only define here if you want to override the auto-detection
5
- *
6
- * Example:
7
- * export const customUpdatableFields: Record<string, string[]> = {
8
- * users: ['name', 'avatar'], // Only these fields can be updated
9
- * }
10
- */
2
+ import type { z } from 'zod';
11
3
  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
4
  export declare const customHiddenFields: Record<string, string[]>;
17
- /**
18
- * Auto-generated model table map
19
- * Automatically includes all tables from schema
20
- */
21
5
  export declare const modelTableMap: Record<string, unknown>;
22
6
  /**
23
- * Gets the table for a given model name
24
- * @param modelName - The name of the model (e.g., 'users', 'products')
25
- * @returns The corresponding database table
26
- * @throws Error if model is not found
7
+ * @throws 404 if modelName is not found in tableMap.
27
8
  */
28
9
  export declare function getTableForModel(modelName: string): SQLiteTable;
29
- /**
30
- * Gets the updatable fields for a model
31
- * @param modelName - The name of the model
32
- * @returns Array of field names that can be updated
33
- */
34
10
  export declare function getUpdatableFields(modelName: string): string[];
35
11
  /**
36
- * Filters an object to only include updatable fields for a model
37
- * @param modelName - The name of the model
38
- * @param data - The data object to filter
39
- * @returns Filtered object with only updatable fields
12
+ * Filters and coerces data for updates, handling timestamp conversion.
40
13
  */
41
14
  export declare function filterUpdatableFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
42
- /**
43
- * Gets the singular name for a model (for error messages)
44
- * Uses pluralize library for accurate singular/plural conversion
45
- * @param modelName - The plural model name
46
- * @returns The singular name in PascalCase
47
- */
48
15
  export declare function getModelSingularName(modelName: string): string;
49
- /**
50
- * Gets the plural name for a model
51
- * @param modelName - The model name (singular or plural)
52
- * @returns The plural name
53
- * @return The plural name
54
- */
55
16
  export declare function getModelPluralName(modelName: string): string;
56
- /**
57
- * Lists all available models
58
- * @returns Array of model names
59
- */
60
17
  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
18
  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
19
  export declare function getPublicColumns(modelName: string): string[] | undefined;
73
20
  /**
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
21
+ * Restricts payload to runtimeConfig resource whitelist and filters hidden fields.
78
22
  */
79
23
  export declare function filterPublicColumns(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
24
+ export declare function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
80
25
  /**
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
26
+ * Generates Zod schema via drizzle-zod, omitting server-managed and protected fields.
85
27
  */
86
- export declare function filterHiddenFields(modelName: string, data: Record<string, unknown>): Record<string, unknown>;
28
+ export declare function getZodSchema(modelName: string, type?: 'insert' | 'patch'): z.ZodObject<any, any>;
@@ -4,15 +4,10 @@ import { pascalCase } from "scule";
4
4
  import { getTableColumns as getDrizzleTableColumns, getTableName } from "drizzle-orm";
5
5
  import { createError } from "h3";
6
6
  import { useRuntimeConfig } from "#imports";
7
- const PROTECTED_FIELDS = ["id", "created_at", "updated_at", "createdAt", "updatedAt"];
8
- const HIDDEN_FIELDS = ["password", "secret", "token"];
9
- export const customUpdatableFields = {
10
- // Add custom field restrictions here if needed
11
- // By default, all fields except PROTECTED_FIELDS are updatable
12
- };
13
- export const customHiddenFields = {
14
- // Add custom hidden fields here if needed
15
- };
7
+ import { createInsertSchema } from "drizzle-zod";
8
+ import { PROTECTED_FIELDS, HIDDEN_FIELDS } from "./constants.js";
9
+ export const customUpdatableFields = {};
10
+ export const customHiddenFields = {};
16
11
  function buildModelTableMap() {
17
12
  const tableMap = {};
18
13
  for (const [key, value] of Object.entries(schema)) {
@@ -56,7 +51,7 @@ export function getUpdatableFields(modelName) {
56
51
  const table = modelTableMap[modelName];
57
52
  if (!table) return [];
58
53
  const allColumns = getTableColumns(table);
59
- return allColumns.filter((col) => !PROTECTED_FIELDS.includes(col));
54
+ return allColumns.filter((col) => !PROTECTED_FIELDS.includes(col) && !HIDDEN_FIELDS.includes(col));
60
55
  }
61
56
  export function filterUpdatableFields(modelName, data) {
62
57
  const allowedFields = getUpdatableFields(modelName);
@@ -86,10 +81,7 @@ export function getAvailableModels() {
86
81
  return Object.keys(modelTableMap);
87
82
  }
88
83
  export function getHiddenFields(modelName) {
89
- if (customHiddenFields[modelName]) {
90
- return customHiddenFields[modelName];
91
- }
92
- return HIDDEN_FIELDS;
84
+ return customHiddenFields[modelName] ?? HIDDEN_FIELDS;
93
85
  }
94
86
  export function getPublicColumns(modelName) {
95
87
  const { resources } = useRuntimeConfig().autoCrud;
@@ -101,8 +93,9 @@ export function filterPublicColumns(modelName, data) {
101
93
  return filterHiddenFields(modelName, data);
102
94
  }
103
95
  const filtered = {};
96
+ const hidden = getHiddenFields(modelName);
104
97
  for (const [key, value] of Object.entries(data)) {
105
- if (publicColumns.includes(key) && !getHiddenFields(modelName).includes(key)) {
98
+ if (publicColumns.includes(key) && !hidden.includes(key)) {
106
99
  filtered[key] = value;
107
100
  }
108
101
  }
@@ -118,3 +111,22 @@ export function filterHiddenFields(modelName, data) {
118
111
  }
119
112
  return filtered;
120
113
  }
114
+ export function getZodSchema(modelName, type = "insert") {
115
+ const table = getTableForModel(modelName);
116
+ const schema2 = createInsertSchema(table);
117
+ if (type === "patch") {
118
+ return schema2.partial();
119
+ }
120
+ const OMIT_ON_CREATE = [
121
+ ...PROTECTED_FIELDS,
122
+ ...HIDDEN_FIELDS
123
+ ];
124
+ const columns = getDrizzleTableColumns(table);
125
+ const fieldsToOmit = {};
126
+ OMIT_ON_CREATE.forEach((field) => {
127
+ if (columns[field]) {
128
+ fieldsToOmit[field] = true;
129
+ }
130
+ });
131
+ return schema2.omit(fieldsToOmit);
132
+ }
@@ -7,11 +7,13 @@ export interface Field {
7
7
  }
8
8
  export declare function drizzleTableToFields(table: any, resourceName: string): {
9
9
  resource: string;
10
+ labelField: string;
10
11
  fields: Field[];
11
12
  };
12
13
  export declare function getRelations(): Promise<Record<string, Record<string, string>>>;
13
14
  export declare function getAllSchemas(): Promise<Record<string, any>>;
14
15
  export declare function getSchema(tableName: string): Promise<{
15
16
  resource: string;
17
+ labelField: string;
16
18
  fields: Field[];
17
19
  } | undefined>;
@@ -15,6 +15,8 @@ export function drizzleTableToFields(table, resourceName) {
15
15
  selectOptions
16
16
  });
17
17
  }
18
+ const fieldNames = fields.map((f) => f.name);
19
+ const labelField = fieldNames.find((n) => n === "name") || fieldNames.find((n) => n === "title") || fieldNames.find((n) => n === "email") || "id";
18
20
  try {
19
21
  const config = getTableConfig(table);
20
22
  config.foreignKeys.forEach((fk) => {
@@ -32,6 +34,8 @@ export function drizzleTableToFields(table, resourceName) {
32
34
  }
33
35
  return {
34
36
  resource: resourceName,
37
+ labelField,
38
+ // metadata point
35
39
  fields
36
40
  };
37
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.24.0",
3
+ "version": "1.26.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",
@@ -51,6 +51,7 @@
51
51
  "@nuxt/scripts": "^0.13.0",
52
52
  "@types/pluralize": "^0.0.33",
53
53
  "c12": "^2.0.1",
54
+ "drizzle-zod": "^0.8.3",
54
55
  "jose": "^5.9.6",
55
56
  "pluralize": "^8.0.0",
56
57
  "scule": "^1.0.0"
@@ -78,7 +79,7 @@
78
79
  "drizzle-orm": "^0.38.3",
79
80
  "eslint": "^9.39.1",
80
81
  "nuxt": "^4.2.1",
81
- "nuxt-auth-utils": "^0.5.26",
82
+ "nuxt-auth-utils": "^0.5.27",
82
83
  "nuxt-authorization": "^0.3.5",
83
84
  "typescript": "~5.9.3",
84
85
  "vitest": "^4.0.13",
@@ -4,7 +4,7 @@ import type { H3Event } from 'h3'
4
4
  // @ts-expect-error - #imports is a virtual alias
5
5
  import { getUserSession } from '#imports'
6
6
  import { eq } from 'drizzle-orm'
7
- import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
7
+ import { getTableForModel, getZodSchema } from '../../utils/modelMapper'
8
8
  import type { TableWithId } from '../../types'
9
9
 
10
10
  // @ts-expect-error - hub:db is a virtual alias
@@ -20,7 +20,8 @@ export default eventHandler(async (event) => {
20
20
  const table = getTableForModel(model) as TableWithId
21
21
 
22
22
  const body = await readBody(event)
23
- const payload = filterUpdatableFields(model, body)
23
+ const schema = getZodSchema(model, 'patch')
24
+ const payload = await schema.parseAsync(body)
24
25
 
25
26
  // Custom check for status update permission
26
27
  if ('status' in payload) {
@@ -1,10 +1,10 @@
1
1
  // server/api/[model]/index.post.ts
2
2
  import { eventHandler, getRouterParams, readBody } from 'h3'
3
3
  import type { H3Event } from 'h3'
4
- // @ts-expect-error - #imports is a virtual alias
4
+ // @ts-expect-error - '#imports' is a virtual alias
5
5
  import { getUserSession } from '#imports'
6
- import { getTableForModel, filterUpdatableFields } from '../../utils/modelMapper'
7
- // @ts-expect-error - hub:db is a virtual alias
6
+ import { getTableForModel, getZodSchema } from '../../utils/modelMapper'
7
+ // @ts-expect-error - 'hub:db' is a virtual alias
8
8
  import { db } from 'hub:db'
9
9
  import { ensureResourceAccess, formatResourceResult, hashPayloadFields } from '../../utils/handler'
10
10
 
@@ -15,7 +15,8 @@ export default eventHandler(async (event) => {
15
15
  const table = getTableForModel(model)
16
16
 
17
17
  const body = await readBody(event)
18
- const payload = filterUpdatableFields(model, body)
18
+ const schema = getZodSchema(model, 'insert')
19
+ const payload = await schema.parseAsync(body)
19
20
 
20
21
  // Custom check for status update permission (or just remove it during creation as per requirement)
21
22
  if ('status' in payload) {
@@ -0,0 +1,111 @@
1
+ import { eventHandler, getQuery, getHeader } from 'h3'
2
+ import { getTableForModel, getAvailableModels } from '../utils/modelMapper'
3
+ import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
4
+ import { getTableConfig } from 'drizzle-orm/sqlite-core'
5
+ import { PROTECTED_FIELDS, HIDDEN_FIELDS } from '../utils/constants'
6
+ // @ts-expect-error - 'hub:db' is a virtual alias
7
+ import { db } from 'hub:db'
8
+ import { ensureAuthenticated } from '../utils/auth'
9
+
10
+ export default eventHandler(async (event) => {
11
+ await ensureAuthenticated(event)
12
+
13
+ const query = getQuery(event)
14
+ const acceptHeader = getHeader(event, 'accept') || ''
15
+
16
+ const models = getAvailableModels().length > 0
17
+ ? getAvailableModels()
18
+ : Object.keys(db?.query || {})
19
+
20
+ const resources = models.map((model) => {
21
+ try {
22
+ const table = getTableForModel(model)
23
+ const columns = getDrizzleTableColumns(table)
24
+ const config = getTableConfig(table)
25
+
26
+ const fields = Object.entries(columns)
27
+ .filter(([name]) => !HIDDEN_FIELDS.includes(name))
28
+ .map(([name, col]) => {
29
+ let references = null
30
+ // @ts-expect-error - Drizzle foreign key internals
31
+ const fk = config?.foreignKeys.find(f => f.reference().columns[0].name === col.name)
32
+
33
+ if (fk) {
34
+ // @ts-expect-error - Drizzle internals
35
+ references = fk.reference().foreignTable[Symbol.for('drizzle:Name')]
36
+ }
37
+ // @ts-expect-error - Drizzle internal referenceConfig
38
+ else if (col.referenceConfig?.foreignTable) {
39
+ // @ts-expect-error - Drizzle internal referenceConfig
40
+ const foreignTable = col.referenceConfig.foreignTable
41
+ references = foreignTable[Symbol.for('drizzle:Name')] || foreignTable.name
42
+ }
43
+
44
+ const semanticType = col.columnType.toLowerCase().replace('sqlite', '')
45
+
46
+ return {
47
+ name,
48
+ type: semanticType,
49
+ required: col.notNull || false,
50
+ isEnum: !!col.enumValues,
51
+ options: col.enumValues || null,
52
+ references,
53
+ isRelation: !!references,
54
+ // Agentic Hint: Is this field writable by the user/agent?
55
+ isReadOnly: PROTECTED_FIELDS.includes(name),
56
+ }
57
+ })
58
+
59
+ const fieldNames = fields.map(f => f.name)
60
+ const labelField = fieldNames.find(n => n === 'name')
61
+ || fieldNames.find(n => n === 'title')
62
+ || fieldNames.find(n => n === 'email')
63
+ || 'id'
64
+
65
+ return {
66
+ resource: model,
67
+ endpoint: `/api/${model}`,
68
+ labelField,
69
+ methods: ['GET', 'POST', 'PATCH', 'DELETE'],
70
+ fields,
71
+ }
72
+ }
73
+ catch {
74
+ return null
75
+ }
76
+ }).filter(Boolean)
77
+
78
+ const payload = {
79
+ architecture: 'Clifland-NAC',
80
+ version: '1.0.0-agentic',
81
+ resources,
82
+ }
83
+
84
+ const currentToken = getQuery(event).token || (getHeader(event, 'authorization')?.split(' ')[1])
85
+ const tokenSuffix = currentToken ? `?token=${currentToken}` : ''
86
+
87
+ // --- CONTENT NEGOTIATION FOR AGENTIC TOOLS ---
88
+ if (query.format === 'md' || acceptHeader.includes('text/markdown')) {
89
+ let markdown = `# ${payload.architecture} API Manifest (v${payload.version})\n\n`
90
+
91
+ payload.resources.forEach((res) => {
92
+ if (!res) return
93
+ markdown += `### Resource: ${res.resource}\n`
94
+ markdown += `- **Endpoint**: \`${res.endpoint}${tokenSuffix}\`\n`
95
+ markdown += `- **Methods**: ${res.methods.join(', ')}\n`
96
+ markdown += `- **Primary Label**: \`${res.labelField}\`\n\n`
97
+ markdown += `| Field | Type | Required | Writable | Details |\n`
98
+ markdown += `| :--- | :--- | :--- | :--- | :--- |\n`
99
+
100
+ res.fields.forEach((f) => {
101
+ const details = f.isEnum && f.options ? `Options: ${f.options.join(', ')}` : (f.references ? `Refs: ${f.references}` : '-')
102
+ markdown += `| ${f.name} | ${f.type} | ${f.required ? '✅' : '❌'} | ${f.isReadOnly ? '❌' : '✅'} | ${details} |\n`
103
+ })
104
+ markdown += `\n---\n`
105
+ })
106
+
107
+ return markdown
108
+ }
109
+
110
+ return payload
111
+ })
@@ -15,6 +15,18 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
15
15
  return true
16
16
  }
17
17
 
18
+ // 1. Bearer Token or Query Check (Agentic/MCP Tooling)
19
+ const authHeader = getHeader(event, 'authorization')
20
+ const query = getQuery(event)
21
+ const apiToken = useRuntimeConfig(event).apiSecretToken
22
+
23
+ // Extract token from Header or fallback to Query param
24
+ const token = (authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : null) || query.token
25
+
26
+ if (token && apiToken && token === apiToken) {
27
+ return true
28
+ }
29
+
18
30
  if (auth.type === 'jwt') {
19
31
  if (!auth.jwtSecret) {
20
32
  console.warn('JWT Secret is not configured but auth type is jwt')
@@ -113,15 +125,28 @@ export async function checkAdminAccess(event: H3Event, model: string, action: st
113
125
 
114
126
  export async function ensureAuthenticated(event: H3Event): Promise<void> {
115
127
  const { auth } = useAutoCrudConfig()
128
+ const runtimeConfig = useRuntimeConfig(event)
116
129
 
117
130
  if (!auth?.authentication) return
118
131
 
119
- if (auth.type === 'jwt' && auth.jwtSecret) {
120
- if (!await verifyJwtToken(event, auth.jwtSecret)) {
121
- throw createError({ statusCode: 401, message: 'Unauthorized' })
122
- }
132
+ // Extract Token: Priority 1: Authorization Header | Priority 2: Query String (?token=)
133
+ const authHeader = getHeader(event, 'authorization')
134
+ const query = getQuery(event)
135
+ const apiToken = runtimeConfig.apiSecretToken
136
+
137
+ const token = (authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : null) || query.token
138
+
139
+ // 1. API Token Check (Agentic/MCP)
140
+ if (token && apiToken && token === apiToken) {
123
141
  return
124
142
  }
125
143
 
144
+ // 2. JWT Check
145
+ if (auth.type === 'jwt' && auth.jwtSecret) {
146
+ if (await verifyJwtToken(event, auth.jwtSecret)) return
147
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
148
+ }
149
+
150
+ // 3. Session Check (Standard UI)
126
151
  await (requireUserSession as (event: H3Event) => Promise<void>)(event)
127
152
  }
@@ -0,0 +1,25 @@
1
+ export const PROTECTED_FIELDS = [
2
+ 'id',
3
+ 'createdAt', 'updatedAt', 'deletedAt',
4
+ 'createdBy', 'updatedBy', 'deletedBy',
5
+ 'created_at', 'updated_at', 'deleted_at',
6
+ 'created_by', 'updated_by', 'deleted_by',
7
+ ]
8
+
9
+ export const HIDDEN_FIELDS = [
10
+ // Sensitive Auth
11
+ 'password',
12
+ 'resetToken', 'reset_token',
13
+ 'resetExpires', 'reset_expires',
14
+ 'githubId', 'github_id',
15
+ 'googleId', 'google_id',
16
+ 'secret',
17
+ 'token',
18
+ // System Fields (Leakage prevention)
19
+ 'deletedAt',
20
+ 'createdBy',
21
+ 'updatedBy',
22
+ 'deleted_at',
23
+ 'created_by',
24
+ 'updated_by',
25
+ ]