nuxt-auto-crud 2.2.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
@@ -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
 
@@ -76,9 +108,9 @@ nuxt dev
76
108
 
77
109
  ---
78
110
 
79
- ## 🌐 Exposed Dynamic RESTful CRUD endpoints
111
+ ## 🌐 Data APIs (Dynamic RESTful CRUD)
80
112
 
81
- Nb: Endpoints follow the pattern `/api/_nac/:model`.
113
+ > Note: All endpoints follow the pattern ${nacEndpointPrefix}/:model. By default, this is /api/_nac/:model.
82
114
 
83
115
  | Method | Endpoint | Action |
84
116
  | --- | --- | --- |
@@ -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,18 +129,27 @@ 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
 
103
- ## 🛠 Frontend Integration APIs
136
+ ## 🛠 Introspection & Metadata APIs
104
137
 
105
- 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.
106
139
 
107
- * **List Resources**: `GET /api/_nac/_schemas` returns all tables (excluding system-protected tables).
108
- * **Resource Metadata**: `GET /api/_nac/_schemas/:resource` returns the field definitions, validation rules, and relationship data for a specific table.
140
+ ### 1. Discovery Endpoints
109
141
 
110
- ---
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`.
111
153
 
112
154
  ### Schema Interface
113
155
 
@@ -153,8 +195,9 @@ Enabling `authentication` in the `autoCrud` config protects all **nac** routes (
153
195
 
154
196
  ### 🔒 Access Control & Data Safety
155
197
 
156
- * **`apiHiddenFields`**: Globally hides sensitive columns from all API responses. Default: `['password', 'secret', 'token', 'reset_token', 'reset_expires', 'github_id', 'google_id']`.
157
- * **`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).
158
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`.
159
202
 
160
203
  ---
@@ -164,11 +207,15 @@ Enabling `authentication` in the `autoCrud` config protects all **nac** routes (
164
207
  | Key | Default | Description |
165
208
  | --- | --- | --- |
166
209
  | `statusFiltering` | `false` | Enables/disables automatic filtering of records based on the `status` column. |
167
- | `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. |
210
+ | `realtime` | `false` | Enables real-time broadcasting of all Create, Update, and Delete (CUD) operations via SSE. |
211
+ | `auth.authentication` | `false` | Requires a valid session for all NAC routes. |
212
+ | `auth.authorization` | `false` | Enables role/owner-based access checks. |
170
213
  | `auth.ownerKey` | `'createdBy'` | The column name used to identify the record creator. |
171
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. |
172
219
  | `nacEndpointPrefix` | `'/api/_nac'` | The base path for NAC routes. Access via `useRuntimeConfig().public.autoCrud`. |
173
220
  | `schemaPath` | `'server/db/schema'` | Location of your Drizzle schema files. |
174
221
 
@@ -179,16 +226,17 @@ autoCrud: {
179
226
  statusFiltering: false,
180
227
  realtime: false,
181
228
  auth: {
182
- authentication: true,
183
- authorization: true,
229
+ authentication: false,
230
+ authorization: false,
184
231
  ownerKey: 'createdBy',
185
232
  },
186
233
  publicResources: {
187
234
  users: ['id', 'name', 'email'],
188
235
  },
189
236
  apiHiddenFields: ['password'],
190
- agenticToken: process.env.NAC_AGENTIC_TOKEN,
191
- formHiddenFields: [],
237
+ formHiddenFields: ['createdAt'], // All fields should be camelCase
238
+ formReadOnlyFields: ['slug', 'externalId'], // Locked for user input
239
+ agenticToken: '',
192
240
  nacEndpointPrefix: '/api/_nac',
193
241
  schemaPath: 'server/db/schema',
194
242
  }
@@ -219,7 +267,7 @@ If `list_active` is present, it applies a hybrid OR logic: users can see all act
219
267
  // Example: Setting context in your Auth Middleware
220
268
  event.context.nac = {
221
269
  userId: user.id,
222
- resourcePermissions: user.permissions[model], // e.g., ['list_own', 'list']
270
+ resourcePermissions: user.permissions[model], // e.g., ['list_own', 'list_active']
223
271
  record: null, // Optional: Pre-fetched record to prevent double-hitting the DB
224
272
  }
225
273
 
@@ -272,7 +320,3 @@ useNacAutoCrudSSE(({ table, action, data: sseData, primaryKey }) => {
272
320
  ```
273
321
  ---
274
322
 
275
- ## ⚠️ Limitations
276
- **Database Support:** Currently optimized for SQLite/libSQL only.
277
-
278
- ---
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.2.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,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;
@@ -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) => {
@@ -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;
@@ -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"];
@@ -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,16 +71,16 @@ 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
- const formHiddenFields = config.public.autoCrud.formHiddenFields;
79
+ const { formHiddenFields, formReadOnlyFields } = config.public.autoCrud;
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
- 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]) => {
81
84
  const zodField = shape[name];
82
85
  const zodTypeName = zodField?._def?.typeName;
83
86
  let type = ZOD_TYPE_MAP[zodTypeName] ?? "string";
@@ -102,7 +105,7 @@ export function getSchemaDefinition(modelName) {
102
105
  selectOptions,
103
106
  required: colInternal.notNull ?? false,
104
107
  references: relations[name],
105
- isReadOnly: formHiddenFields.includes(name)
108
+ isReadOnly: formReadOnlyFields.includes(name) || name === "id"
106
109
  };
107
110
  });
108
111
  return {
@@ -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.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",
@@ -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",