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 +69 -25
- 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.d.ts +13 -4
- package/dist/runtime/server/api/_nac/_meta.get.js +6 -4
- package/dist/runtime/server/api/_nac/_schemas/[model].get.d.ts +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.d.ts +4 -6
- package/dist/runtime/server/utils/modelMapper.js +10 -7
- package/dist/runtime/server/utils/queries.js +26 -2
- package/package.json +2 -2
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
|
-
## 🌐
|
|
111
|
+
## 🌐 Data APIs (Dynamic RESTful CRUD)
|
|
80
112
|
|
|
81
|
-
|
|
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` |
|
|
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`
|
|
132
|
+
| **Delete** | `DELETE` | `/api/_nac/users/1` | User `1` removed from DB |
|
|
100
133
|
|
|
101
134
|
---
|
|
102
135
|
|
|
103
|
-
## 🛠
|
|
136
|
+
## 🛠 Introspection & Metadata APIs
|
|
104
137
|
|
|
105
|
-
|
|
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
|
-
|
|
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:
|
|
157
|
-
* **`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).
|
|
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
|
|
168
|
-
| `auth.authentication` | `
|
|
169
|
-
| `auth.authorization` | `
|
|
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:
|
|
183
|
-
authorization:
|
|
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
|
-
|
|
191
|
-
|
|
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', '
|
|
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
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:
|
|
7
|
+
labelField: string;
|
|
8
8
|
methods: string[];
|
|
9
|
-
fields:
|
|
10
|
-
|
|
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
|
|
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
|
-
})
|
|
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<
|
|
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 (!
|
|
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"];
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type Column, Table } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
3
|
-
import type { SchemaDefinition } from '
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|