nuxt-auto-crud 2.2.0 → 2.3.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
@@ -1,4 +1,4 @@
1
- # nuxt-auto-crud (nac 2.x)
1
+ # nuxt-auto-crud (nac 2.x beta)
2
2
 
3
3
  **Zero-Codegen Dynamic RESTful CRUD APIs** derived directly from schemas. It eliminates the need to manually write or generate boilerplate for CRUD operations.
4
4
 
@@ -11,13 +11,25 @@
11
11
  * **Constant Bundle Size**: Since no code is generated, the bundle size remains virtually identical whether you have one table or one hundred (scaling only with your schema definitions).
12
12
  ---
13
13
 
14
+ ## Supported Databases
15
+ * **SQLite (libSQL)**
16
+ * **MySQL**
17
+
18
+ ---
19
+
14
20
  ## Installation Guide
15
21
 
16
22
  ### Option A: Starter Template
23
+ #### SQLite
17
24
  ```bash
18
25
  npx nuxi init -t gh:clifordpereira/nac-starter my-app
19
26
  ```
20
27
 
28
+ #### MySQL
29
+ ```bash
30
+ npx nuxi init -t gh:clifordpereira/nac-starter-mysql my-app
31
+ ```
32
+
21
33
  ### Option B: Manual Installation
22
34
 
23
35
  ```bash
@@ -27,6 +39,7 @@ bun add drizzle-orm@beta @libsql/client nuxt-auto-crud
27
39
  bun add -D drizzle-kit@beta
28
40
 
29
41
  ```
42
+ > Mysql users may replace `@libsql/client` with `mysql2`
30
43
 
31
44
  #### Configuration
32
45
 
@@ -40,12 +53,13 @@ export default defineNuxtConfig({
40
53
  ],
41
54
  hub: {
42
55
  db: 'sqlite'
56
+ // db: 'mysql'
43
57
  }
44
58
  })
45
59
 
46
60
  ```
47
61
 
48
- #### Schema Definition
62
+ #### Schema Definition (SQLite)
49
63
 
50
64
  Define your schema in `server/db/schema.ts`:
51
65
 
@@ -63,6 +77,24 @@ export const users = sqliteTable('users', {
63
77
 
64
78
  ```
65
79
 
80
+ #### Schema Definition (MySQL)
81
+
82
+ Define your schema in `server/db/schema.ts`:
83
+
84
+ ```typescript
85
+ import { mysqlTable, text, serial, timestamp } from 'drizzle-orm/mysql-core'
86
+
87
+ export const users = mysqlTable('users', {
88
+ id: serial().primaryKey(),
89
+ name: text().notNull(),
90
+ email: text().notNull().unique(),
91
+ password: text().notNull(),
92
+ avatar: text().notNull(),
93
+ createdAt: timestamp().notNull().defaultNow(),
94
+ })
95
+
96
+ ```
97
+
66
98
  ### Generate Migrations and Start Dev Server
67
99
  After installing (either option), run the following commands:
68
100
 
@@ -86,7 +118,8 @@ Nb: Endpoints follow the pattern `/api/_nac/:model`.
86
118
  | **POST** | `/:model` | Create record with Zod validation |
87
119
  | **GET** | `/:model/:id` | Fetch single record |
88
120
  | **PATCH** | `/:model/:id` | Partial update with validation |
89
- | **DELETE** | `/:model/:id` | Hard delete |
121
+ | **DELETE** | `/:model/:id` | Delete record |
122
+
90
123
 
91
124
  **Example (`users` table):**
92
125
 
@@ -96,7 +129,7 @@ Nb: Endpoints follow the pattern `/api/_nac/:model`.
96
129
  | **Create** | `POST` | `/api/_nac/users` | New user record added |
97
130
  | **Fetch One** | `GET` | `/api/_nac/users/1` | Details of user with `id: 1` |
98
131
  | **Update** | `PATCH` | `/api/_nac/users/1` | Partial update to user `1` |
99
- | **Delete** | `DELETE` | `/api/_nac/users/1` | User `1` hard deleted from DB |
132
+ | **Delete** | `DELETE` | `/api/_nac/users/1` | User `1` removed from DB |
100
133
 
101
134
  ---
102
135
 
@@ -106,6 +139,7 @@ In addition to CRUD endpoints, **nac** provides metadata APIs to power dynamic f
106
139
 
107
140
  * **List Resources**: `GET /api/_nac/_schemas` returns all tables (excluding system-protected tables).
108
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.
109
143
 
110
144
  ---
111
145
 
@@ -165,8 +199,8 @@ Enabling `authentication` in the `autoCrud` config protects all **nac** routes (
165
199
  | --- | --- | --- |
166
200
  | `statusFiltering` | `false` | Enables/disables automatic filtering of records based on the `status` column. |
167
201
  | `realtime` | `false` | Enables/disables real-time capabilities. |
168
- | `auth.authentication` | `true` | Requires a valid session for all NAC routes. |
169
- | `auth.authorization` | `true` | Enables role/owner-based access checks. |
202
+ | `auth.authentication` | `false` | Requires a valid session for all NAC routes. |
203
+ | `auth.authorization` | `false` | Enables role/owner-based access checks. |
170
204
  | `auth.ownerKey` | `'createdBy'` | The column name used to identify the record creator. |
171
205
  | `publicResources` | `{}` | Defines tables and specific columns accessible without auth. |
172
206
  | `nacEndpointPrefix` | `'/api/_nac'` | The base path for NAC routes. Access via `useRuntimeConfig().public.autoCrud`. |
@@ -179,8 +213,8 @@ autoCrud: {
179
213
  statusFiltering: false,
180
214
  realtime: false,
181
215
  auth: {
182
- authentication: true,
183
- authorization: true,
216
+ authentication: false,
217
+ authorization: false,
184
218
  ownerKey: 'createdBy',
185
219
  },
186
220
  publicResources: {
@@ -272,7 +306,6 @@ useNacAutoCrudSSE(({ table, action, data: sseData, primaryKey }) => {
272
306
  ```
273
307
  ---
274
308
 
275
- ## ⚠️ Limitations
276
- **Database Support:** Currently optimized for SQLite/libSQL only.
309
+
277
310
 
278
311
  ---
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
3
  "configKey": "autoCrud",
4
- "version": "2.2.0",
4
+ "version": "2.3.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1,12 +1,21 @@
1
1
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | {
2
2
  architecture: string;
3
3
  version: string;
4
- resources: ({
4
+ resources: {
5
5
  resource: string;
6
6
  endpoint: string;
7
- labelField: any;
7
+ labelField: string;
8
8
  methods: string[];
9
- fields: any;
10
- } | null)[];
9
+ fields: {
10
+ name: string;
11
+ type: string;
12
+ required: boolean | undefined;
13
+ isEnum: boolean;
14
+ options: string[] | null;
15
+ references: string | null;
16
+ isRelation: boolean;
17
+ isReadOnly: boolean;
18
+ }[];
19
+ }[];
11
20
  }>>;
12
21
  export default _default;
@@ -9,9 +9,9 @@ export default eventHandler(async (event) => {
9
9
  const acceptHeader = getHeader(event, "accept") || "";
10
10
  const availableModels = Object.keys(modelTableMap);
11
11
  const models = availableModels.length > 0 ? availableModels : Object.keys(db?.query || {});
12
- const resources = models.map((model) => {
12
+ const resourcesResults = await Promise.all(models.map(async (model) => {
13
13
  try {
14
- const schema = getSchemaDefinition(model);
14
+ const schema = await getSchemaDefinition(model);
15
15
  const fields = schema.fields.map((field) => ({
16
16
  name: field.name,
17
17
  type: field.type,
@@ -32,13 +32,15 @@ export default eventHandler(async (event) => {
32
32
  } catch {
33
33
  return null;
34
34
  }
35
- }).filter(Boolean);
35
+ }));
36
+ const resources = resourcesResults.filter((res) => res !== null);
36
37
  const payload = {
37
38
  architecture: "Clifland-NAC",
38
39
  version: "1.0.0-agentic",
39
40
  resources
40
41
  };
41
42
  if (query.format === "md" || acceptHeader.includes("text/markdown")) {
43
+ setResponseHeader(event, "Content-Type", "text/markdown; charset=utf-8");
42
44
  let markdown = `# ${payload.architecture} API Manifest (v${payload.version})
43
45
 
44
46
  `;
@@ -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<import("../../../../shared/utils/types.js").SchemaDefinition>>;
2
2
  export default _default;
@@ -1,8 +1,7 @@
1
1
  import { type Column, Table } from 'drizzle-orm';
2
- import { getTableConfig } from 'drizzle-orm/sqlite-core';
3
- import type { SchemaDefinition } from '#nac/shared/utils/types';
2
+ import type { ForeignKey } from 'drizzle-orm/sqlite-core';
3
+ import type { SchemaDefinition } from '../../shared/utils/types.js';
4
4
  import type { QueryContext } from '../../types/index.js';
5
- type ForeignKey = ReturnType<typeof getTableConfig>['foreignKeys'][number];
6
5
  /**
7
6
  * Builds a map of all exported Drizzle tables from the schema.
8
7
  * @returns {Record<string, Table>} A mapping of export keys to their corresponding Table instances.
@@ -26,7 +25,7 @@ export declare function getSelectableFields(table: Table, context?: QueryContext
26
25
  * Resolves table relationships for NAC reflection.
27
26
  * Maps property keys to target table names.
28
27
  */
29
- export declare function resolveTableRelations(table: Table): Record<string, string>;
28
+ export declare function resolveTableRelations(table: Table): Promise<Record<string, string>>;
30
29
  /**
31
30
  * Resolves the label field for a model.
32
31
  * @param columnNames The names of the columns in the model
@@ -40,5 +39,4 @@ export declare function getLabelField(columnNames: string[]): string;
40
39
  * - Infers types from Drizzle columns
41
40
  * - Resolves foreign key relations
42
41
  */
43
- export declare function getSchemaDefinition(modelName: string): SchemaDefinition;
44
- export {};
42
+ export declare function getSchemaDefinition(modelName: string): Promise<SchemaDefinition>;
@@ -1,5 +1,4 @@
1
1
  import { getColumns, Table, is, getTableName } from "drizzle-orm";
2
- import { getTableConfig } from "drizzle-orm/sqlite-core";
3
2
  import { createInsertSchema } from "drizzle-zod";
4
3
  import { useRuntimeConfig } from "#imports";
5
4
  import * as schema from "#nac/schema";
@@ -42,7 +41,11 @@ export function getSelectableFields(table, context = {}) {
42
41
  }
43
42
  return result;
44
43
  }
45
- export function resolveTableRelations(table) {
44
+ export async function resolveTableRelations(table) {
45
+ const { hub } = useRuntimeConfig();
46
+ const dbConfig = hub.db;
47
+ const isMysql = dbConfig === "mysql" || typeof dbConfig === "object" && dbConfig?.dialect === "mysql";
48
+ const { getTableConfig } = await (isMysql ? import("drizzle-orm/mysql-core") : import("drizzle-orm/sqlite-core"));
46
49
  const config = getTableConfig(table);
47
50
  const columnsMap = getColumns(table);
48
51
  const relations = {};
@@ -68,14 +71,14 @@ const SEMANTIC_CHECK_MAP = {
68
71
  url: "url"
69
72
  };
70
73
  const TEXTAREA_HINTS = ["content", "description", "bio", "message"];
71
- export function getSchemaDefinition(modelName) {
74
+ export async function getSchemaDefinition(modelName) {
72
75
  const table = modelTableMap[modelName];
73
76
  if (!table) throw new ResourceNotFoundError(modelName);
74
77
  const config = useRuntimeConfig();
75
78
  const apiHiddenFields = config.autoCrud.apiHiddenFields;
76
79
  const formHiddenFields = config.public.autoCrud.formHiddenFields;
77
80
  const columns = getColumns(table);
78
- const relations = resolveTableRelations(table);
81
+ const relations = await resolveTableRelations(table);
79
82
  const shape = createInsertSchema(table).shape;
80
83
  const fields = Object.entries(columns).filter(([name]) => !apiHiddenFields.includes(name)).map(([name, col]) => {
81
84
  const zodField = shape[name];
@@ -4,6 +4,11 @@ import { eq, desc, and, or, getColumns } from "drizzle-orm";
4
4
  import { getSelectableFields } from "./modelMapper.js";
5
5
  import { DeletionFailedError, InsertionFailedError, RecordNotFoundError, UnauthorizedAccessError, UpdateFailedError } from "../exceptions.js";
6
6
  import { pick } from "#nac/shared/utils/helpers";
7
+ function isMysql() {
8
+ const hub = useRuntimeConfig().hub;
9
+ const dbConfig = hub?.db;
10
+ return dbConfig === "mysql" || typeof dbConfig === "object" && dbConfig?.dialect === "mysql";
11
+ }
7
12
  export function getVisibilityFilters(table, context = {}) {
8
13
  const isAuthorizationEnabled = useRuntimeConfig().autoCrud.auth?.authorization;
9
14
  const isStatusFilteringEnabled = useRuntimeConfig().autoCrud.statusFiltering;
@@ -46,14 +51,19 @@ export async function nacGetRows(table, context = {}) {
46
51
  let query = db.select(fields).from(table).$dynamic();
47
52
  const filters = getVisibilityFilters(table, context);
48
53
  if (filters.length > 0) query = query.where(and(...filters));
49
- return await query.orderBy(desc(table.id)).all();
54
+ const baseQuery = query.orderBy(desc(table.id));
55
+ if (isMysql()) {
56
+ return await baseQuery;
57
+ }
58
+ return await baseQuery.all();
50
59
  }
51
60
  export async function nacGetRow(table, id, context = {}) {
52
61
  const selectableFields = getSelectableFields(table, context);
53
62
  if (context.record) {
54
63
  return pick(context.record, Object.keys(selectableFields));
55
64
  }
56
- const record = await db.select(selectableFields).from(table).where(eq(table.id, Number(id))).get();
65
+ const query = db.select(selectableFields).from(table).where(eq(table.id, Number(id)));
66
+ const record = isMysql() ? (await query)[0] : await query.get();
57
67
  if (!record) throw new RecordNotFoundError();
58
68
  return record;
59
69
  }
@@ -69,6 +79,11 @@ export async function nacCreateRow(table, data, context = {}) {
69
79
  if ("updatedAt" in allColumns) {
70
80
  payload.updatedAt = /* @__PURE__ */ new Date();
71
81
  }
82
+ if (isMysql()) {
83
+ const [res] = await db.insert(table).values(payload);
84
+ const rows = await db.select(selectableFields).from(table).where(eq(table.id, res.insertId));
85
+ return rows[0];
86
+ }
72
87
  const result = await db.insert(table).values(payload).returning(selectableFields).get();
73
88
  if (!result) throw new InsertionFailedError();
74
89
  return result;
@@ -84,6 +99,10 @@ export async function nacUpdateRow(table, id, data, context = {}) {
84
99
  if ("updatedAt" in allColumns) {
85
100
  payload.updatedAt = /* @__PURE__ */ new Date();
86
101
  }
102
+ if (isMysql()) {
103
+ await db.update(table).set(payload).where(eq(table.id, targetId));
104
+ return await nacGetRow(table, id, context);
105
+ }
87
106
  const [updated] = await db.update(table).set(payload).where(eq(table.id, targetId)).returning(selectableFields);
88
107
  if (!updated) throw new UpdateFailedError();
89
108
  return updated;
@@ -91,6 +110,11 @@ export async function nacUpdateRow(table, id, data, context = {}) {
91
110
  export async function nacDeleteRow(table, id) {
92
111
  const targetId = Number(id);
93
112
  const fields = getSelectableFields(table);
113
+ if (isMysql()) {
114
+ const recordToDelete = await nacGetRow(table, id);
115
+ await db.delete(table).where(eq(table.id, targetId));
116
+ return recordToDelete;
117
+ }
94
118
  const deletedRecord = await db.delete(table).where(eq(table.id, targetId)).returning(fields).get();
95
119
  if (!deletedRecord) throw new DeletionFailedError();
96
120
  return deletedRecord;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auto-crud",
3
- "version": "2.2.0",
3
+ "version": "2.3.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",
@@ -49,11 +49,11 @@
49
49
  "dependencies": {
50
50
  "@nuxt/kit": "^4.3.1",
51
51
  "drizzle-zod": "^0.8.3",
52
+ "mysql2": "^3.18.2",
52
53
  "pluralize": "^8.0.0",
53
54
  "zod": "^4.3.6"
54
55
  },
55
56
  "peerDependencies": {
56
- "@libsql/client": "^0.17.0",
57
57
  "@nuxthub/core": "^0.10.6",
58
58
  "drizzle-kit": ">=1.0.0-beta.0",
59
59
  "drizzle-orm": ">=1.0.0-beta.0",