nuxt-auto-crud 1.25.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.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "1.25.0",
4
+ "version": "1.26.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,10 +1,11 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | {
2
2
  architecture: string;
3
3
  version: string;
4
4
  resources: ({
5
5
  resource: string;
6
6
  endpoint: string;
7
7
  labelField: string;
8
+ methods: string[];
8
9
  fields: {
9
10
  name: string;
10
11
  type: any;
@@ -13,6 +14,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
13
14
  options: any;
14
15
  references: any;
15
16
  isRelation: boolean;
17
+ isReadOnly: boolean;
16
18
  }[];
17
19
  } | null)[];
18
20
  }>>;
@@ -1,4 +1,4 @@
1
- import { eventHandler } from "h3";
1
+ import { eventHandler, getQuery, getHeader } from "h3";
2
2
  import { getTableForModel, getAvailableModels } from "../utils/modelMapper.js";
3
3
  import { getTableColumns as getDrizzleTableColumns } from "drizzle-orm";
4
4
  import { getTableConfig } from "drizzle-orm/sqlite-core";
@@ -7,17 +7,17 @@ import { db } from "hub:db";
7
7
  import { ensureAuthenticated } from "../utils/auth.js";
8
8
  export default eventHandler(async (event) => {
9
9
  await ensureAuthenticated(event);
10
+ const query = getQuery(event);
11
+ const acceptHeader = getHeader(event, "accept") || "";
10
12
  const models = getAvailableModels().length > 0 ? getAvailableModels() : Object.keys(db?.query || {});
11
13
  const resources = models.map((model) => {
12
14
  try {
13
15
  const table = getTableForModel(model);
14
16
  const columns = getDrizzleTableColumns(table);
15
17
  const config = getTableConfig(table);
16
- const fields = Object.entries(columns).filter(([name]) => !PROTECTED_FIELDS.includes(name) && !HIDDEN_FIELDS.includes(name)).map(([name, col]) => {
18
+ const fields = Object.entries(columns).filter(([name]) => !HIDDEN_FIELDS.includes(name)).map(([name, col]) => {
17
19
  let references = null;
18
- const fk = config?.foreignKeys.find(
19
- (f) => f.reference().columns[0].name === col.name
20
- );
20
+ const fk = config?.foreignKeys.find((f) => f.reference().columns[0].name === col.name);
21
21
  if (fk) {
22
22
  references = fk.reference().foreignTable[Symbol.for("drizzle:Name")];
23
23
  } else if (col.referenceConfig?.foreignTable) {
@@ -32,7 +32,9 @@ export default eventHandler(async (event) => {
32
32
  isEnum: !!col.enumValues,
33
33
  options: col.enumValues || null,
34
34
  references,
35
- isRelation: !!references
35
+ isRelation: !!references,
36
+ // Agentic Hint: Is this field writable by the user/agent?
37
+ isReadOnly: PROTECTED_FIELDS.includes(name)
36
38
  };
37
39
  });
38
40
  const fieldNames = fields.map((f) => f.name);
@@ -41,15 +43,49 @@ export default eventHandler(async (event) => {
41
43
  resource: model,
42
44
  endpoint: `/api/${model}`,
43
45
  labelField,
46
+ methods: ["GET", "POST", "PATCH", "DELETE"],
44
47
  fields
45
48
  };
46
49
  } catch {
47
50
  return null;
48
51
  }
49
52
  }).filter(Boolean);
50
- return {
53
+ const payload = {
51
54
  architecture: "Clifland-NAC",
52
55
  version: "1.0.0-agentic",
53
56
  resources
54
57
  };
58
+ const currentToken = getQuery(event).token || getHeader(event, "authorization")?.split(" ")[1];
59
+ const tokenSuffix = currentToken ? `?token=${currentToken}` : "";
60
+ if (query.format === "md" || acceptHeader.includes("text/markdown")) {
61
+ let markdown = `# ${payload.architecture} API Manifest (v${payload.version})
62
+
63
+ `;
64
+ payload.resources.forEach((res) => {
65
+ if (!res) return;
66
+ markdown += `### Resource: ${res.resource}
67
+ `;
68
+ markdown += `- **Endpoint**: \`${res.endpoint}${tokenSuffix}\`
69
+ `;
70
+ markdown += `- **Methods**: ${res.methods.join(", ")}
71
+ `;
72
+ markdown += `- **Primary Label**: \`${res.labelField}\`
73
+
74
+ `;
75
+ markdown += `| Field | Type | Required | Writable | Details |
76
+ `;
77
+ markdown += `| :--- | :--- | :--- | :--- | :--- |
78
+ `;
79
+ res.fields.forEach((f) => {
80
+ const details = f.isEnum && f.options ? `Options: ${f.options.join(", ")}` : f.references ? `Refs: ${f.references}` : "-";
81
+ markdown += `| ${f.name} | ${f.type} | ${f.required ? "\u2705" : "\u274C"} | ${f.isReadOnly ? "\u274C" : "\u2705"} | ${details} |
82
+ `;
83
+ });
84
+ markdown += `
85
+ ---
86
+ `;
87
+ });
88
+ return markdown;
89
+ }
90
+ return payload;
55
91
  });
@@ -7,6 +7,13 @@ export async function checkAdminAccess(event, model, action, context) {
7
7
  if (!auth?.authentication) {
8
8
  return true;
9
9
  }
10
+ const authHeader = getHeader(event, "authorization");
11
+ const query = getQuery(event);
12
+ const apiToken = useRuntimeConfig(event).apiSecretToken;
13
+ const token = (authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null) || query.token;
14
+ if (token && apiToken && token === apiToken) {
15
+ return true;
16
+ }
10
17
  if (auth.type === "jwt") {
11
18
  if (!auth.jwtSecret) {
12
19
  console.warn("JWT Secret is not configured but auth type is jwt");
@@ -40,8 +47,8 @@ export async function checkAdminAccess(event, model, action, context) {
40
47
  const hasCreatedBy = "createdBy" in table;
41
48
  const hasUserId = "userId" in table;
42
49
  if (hasCreatedBy || hasUserId) {
43
- const query = db.select().from(table).where(eq(table.id, Number(context.id)));
44
- const record = await query.get();
50
+ const query2 = db.select().from(table).where(eq(table.id, Number(context.id)));
51
+ const record = await query2.get();
45
52
  if (record) {
46
53
  if (hasCreatedBy) {
47
54
  if (String(record.createdBy) === String(user.id)) return true;
@@ -73,12 +80,18 @@ export async function checkAdminAccess(event, model, action, context) {
73
80
  }
74
81
  export async function ensureAuthenticated(event) {
75
82
  const { auth } = useAutoCrudConfig();
83
+ const runtimeConfig = useRuntimeConfig(event);
76
84
  if (!auth?.authentication) return;
77
- if (auth.type === "jwt" && auth.jwtSecret) {
78
- if (!await verifyJwtToken(event, auth.jwtSecret)) {
79
- throw createError({ statusCode: 401, message: "Unauthorized" });
80
- }
85
+ const authHeader = getHeader(event, "authorization");
86
+ const query = getQuery(event);
87
+ const apiToken = runtimeConfig.apiSecretToken;
88
+ const token = (authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null) || query.token;
89
+ if (token && apiToken && token === apiToken) {
81
90
  return;
82
91
  }
92
+ if (auth.type === "jwt" && auth.jwtSecret) {
93
+ if (await verifyJwtToken(event, auth.jwtSecret)) return;
94
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
95
+ }
83
96
  await requireUserSession(event);
84
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "1.25.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",
@@ -1,5 +1,4 @@
1
- // server/api/_meta.get.ts
2
- import { eventHandler } from 'h3'
1
+ import { eventHandler, getQuery, getHeader } from 'h3'
3
2
  import { getTableForModel, getAvailableModels } from '../utils/modelMapper'
4
3
  import { getTableColumns as getDrizzleTableColumns } from 'drizzle-orm'
5
4
  import { getTableConfig } from 'drizzle-orm/sqlite-core'
@@ -11,6 +10,9 @@ import { ensureAuthenticated } from '../utils/auth'
11
10
  export default eventHandler(async (event) => {
12
11
  await ensureAuthenticated(event)
13
12
 
13
+ const query = getQuery(event)
14
+ const acceptHeader = getHeader(event, 'accept') || ''
15
+
14
16
  const models = getAvailableModels().length > 0
15
17
  ? getAvailableModels()
16
18
  : Object.keys(db?.query || {})
@@ -21,31 +23,24 @@ export default eventHandler(async (event) => {
21
23
  const columns = getDrizzleTableColumns(table)
22
24
  const config = getTableConfig(table)
23
25
 
24
- // Map columns to fields
25
26
  const fields = Object.entries(columns)
26
- .filter(([name]) => !PROTECTED_FIELDS.includes(name) && !HIDDEN_FIELDS.includes(name))
27
+ .filter(([name]) => !HIDDEN_FIELDS.includes(name))
27
28
  .map(([name, col]) => {
28
29
  let references = null
29
-
30
- // 1. Check for Foreign Keys via getTableConfig (Robust Drizzle Reflection)
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- const fk = config?.foreignKeys.find((f: any) =>
33
- f.reference().columns[0].name === col.name,
34
- )
30
+ // @ts-expect-error - Drizzle foreign key internals
31
+ const fk = config?.foreignKeys.find(f => f.reference().columns[0].name === col.name)
35
32
 
36
33
  if (fk) {
37
34
  // @ts-expect-error - Drizzle internals
38
35
  references = fk.reference().foreignTable[Symbol.for('drizzle:Name')]
39
36
  }
40
- // 2. Fallback to inline reference config if symbol lookup fails
41
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
- else if ((col as any).referenceConfig?.foreignTable) {
43
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
- const foreignTable = (col as any).referenceConfig.foreignTable
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
45
41
  references = foreignTable[Symbol.for('drizzle:Name')] || foreignTable.name
46
42
  }
47
43
 
48
- // Semantic Normalization
49
44
  const semanticType = col.columnType.toLowerCase().replace('sqlite', '')
50
45
 
51
46
  return {
@@ -56,10 +51,11 @@ export default eventHandler(async (event) => {
56
51
  options: col.enumValues || null,
57
52
  references,
58
53
  isRelation: !!references,
54
+ // Agentic Hint: Is this field writable by the user/agent?
55
+ isReadOnly: PROTECTED_FIELDS.includes(name),
59
56
  }
60
57
  })
61
58
 
62
- // 3. Implement Clifland Label Heuristic (name > title > email > id)
63
59
  const fieldNames = fields.map(f => f.name)
64
60
  const labelField = fieldNames.find(n => n === 'name')
65
61
  || fieldNames.find(n => n === 'title')
@@ -70,6 +66,7 @@ export default eventHandler(async (event) => {
70
66
  resource: model,
71
67
  endpoint: `/api/${model}`,
72
68
  labelField,
69
+ methods: ['GET', 'POST', 'PATCH', 'DELETE'],
73
70
  fields,
74
71
  }
75
72
  }
@@ -78,9 +75,37 @@ export default eventHandler(async (event) => {
78
75
  }
79
76
  }).filter(Boolean)
80
77
 
81
- return {
78
+ const payload = {
82
79
  architecture: 'Clifland-NAC',
83
80
  version: '1.0.0-agentic',
84
81
  resources,
85
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
86
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
  }