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 +43 -10
- package/dist/module.json +1 -1
- package/dist/runtime/server/api/_nac/_meta.get.d.ts +13 -4
- package/dist/runtime/server/api/_nac/_meta.get.js +5 -3
- package/dist/runtime/server/api/_nac/_schemas/[model].get.d.ts +1 -1
- package/dist/runtime/server/utils/modelMapper.d.ts +4 -6
- package/dist/runtime/server/utils/modelMapper.js +7 -4
- 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
|
|
|
@@ -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,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`
|
|
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` | `
|
|
169
|
-
| `auth.authorization` | `
|
|
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:
|
|
183
|
-
authorization:
|
|
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
|
-
|
|
276
|
-
**Database Support:** Currently optimized for SQLite/libSQL only.
|
|
309
|
+
|
|
277
310
|
|
|
278
311
|
---
|
package/dist/module.json
CHANGED
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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,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
|
-
|
|
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.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",
|