@nexa-stack/framework 1.0.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/.env.example +46 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/bin/nexa.mjs +41 -0
- package/bin/nexa.ts +334 -0
- package/docs/AI.md +69 -0
- package/docs/ARCHITECTURE.md +74 -0
- package/docs/EXAMPLES.md +114 -0
- package/docs/FRAMEWORK.md +226 -0
- package/docs/LANGUAGE.md +39 -0
- package/docs/README.md +7 -0
- package/docs/READY.md +51 -0
- package/docs/REFERENCE.md +255 -0
- package/docs/START.md +98 -0
- package/docs/advanced.md +97 -0
- package/docs/authentication.md +54 -0
- package/docs/cli.md +15 -0
- package/docs/compare.md +51 -0
- package/docs/configuration.md +65 -0
- package/docs/database.md +396 -0
- package/docs/installation.md +57 -0
- package/docs/localization.md +47 -0
- package/docs/resources.md +75 -0
- package/docs/routing.md +59 -0
- package/docs/seeding.md +33 -0
- package/docs/services.md +146 -0
- package/package.json +77 -0
- package/packages/auth/src/auth.test.ts +23 -0
- package/packages/auth/src/auth.ts +287 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/cache/src/index.ts +203 -0
- package/packages/client/src/index.ts +49 -0
- package/packages/core/src/app.ts +97 -0
- package/packages/core/src/config.ts +55 -0
- package/packages/core/src/dev.ts +104 -0
- package/packages/core/src/fields.test.ts +81 -0
- package/packages/core/src/fields.ts +309 -0
- package/packages/core/src/index.ts +60 -0
- package/packages/core/src/lang.ts +79 -0
- package/packages/core/src/loader.ts +42 -0
- package/packages/core/src/migrate.ts +91 -0
- package/packages/core/src/policy.ts +36 -0
- package/packages/core/src/registry.ts +17 -0
- package/packages/core/src/reload.ts +92 -0
- package/packages/core/src/resource.test.ts +32 -0
- package/packages/core/src/resource.ts +87 -0
- package/packages/core/src/routes.ts +22 -0
- package/packages/core/src/runtime.ts +11 -0
- package/packages/database/src/builder.ts +266 -0
- package/packages/database/src/database.ts +252 -0
- package/packages/database/src/dialect.ts +186 -0
- package/packages/database/src/index.ts +6 -0
- package/packages/database/src/mysql.ts +114 -0
- package/packages/database/src/postgres.ts +117 -0
- package/packages/database/src/query.ts +115 -0
- package/packages/database/src/sqlite.ts +216 -0
- package/packages/database/src/types.ts +104 -0
- package/packages/events/src/index.ts +17 -0
- package/packages/export/src/index.ts +36 -0
- package/packages/log/src/index.ts +38 -0
- package/packages/mail/src/index.ts +130 -0
- package/packages/notifications/src/index.ts +84 -0
- package/packages/plugins/src/index.ts +39 -0
- package/packages/queue/src/index.ts +185 -0
- package/packages/queue/src/jobs.ts +9 -0
- package/packages/schedule/src/index.ts +64 -0
- package/packages/server/src/index.ts +1 -0
- package/packages/server/src/middleware.ts +143 -0
- package/packages/server/src/query.ts +40 -0
- package/packages/server/src/router.ts +813 -0
- package/packages/sms/src/index.ts +33 -0
- package/packages/storage/src/upload.ts +36 -0
- package/packages/testing/src/index.ts +67 -0
- package/packages/validation/src/index.ts +1 -0
- package/packages/validation/src/validate.test.ts +35 -0
- package/packages/validation/src/validate.ts +112 -0
- package/public/admin.html +369 -0
- package/public/compare.html +66 -0
- package/public/dev-bar.js +213 -0
- package/public/docs.html +315 -0
- package/public/index.html +66 -0
package/docs/EXAMPLES.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
## ERP (full)
|
|
4
|
+
|
|
5
|
+
See current `app.ts` in project root — 7 modules with relations and seed data.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun run dev
|
|
9
|
+
# → http://localhost:3333/admin
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## School (minimal)
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { start, resource, string, number, email, belongsTo } from "./packages/core/src/index.js";
|
|
18
|
+
|
|
19
|
+
resource("students", {
|
|
20
|
+
name: string().required(),
|
|
21
|
+
email: email().required(),
|
|
22
|
+
age: number().required(),
|
|
23
|
+
}, { label: "Students" }).admin();
|
|
24
|
+
|
|
25
|
+
resource("courses", {
|
|
26
|
+
title: string().required(),
|
|
27
|
+
price: number().required(),
|
|
28
|
+
}, { label: "Courses" }).admin();
|
|
29
|
+
|
|
30
|
+
resource("enrollments", {
|
|
31
|
+
student: belongsTo("students").required(),
|
|
32
|
+
course: belongsTo("courses").required(),
|
|
33
|
+
}, { label: "Enrollments" }).admin().auth();
|
|
34
|
+
|
|
35
|
+
await start("./school.db");
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Shop (minimal)
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { start, resource, string, money, getDb } from "./packages/core/src/index.js";
|
|
44
|
+
|
|
45
|
+
resource("products", {
|
|
46
|
+
name: string().required(),
|
|
47
|
+
price: money().required(),
|
|
48
|
+
}, { label: "Products" }).admin();
|
|
49
|
+
|
|
50
|
+
resource("sales", {
|
|
51
|
+
total: money().required(),
|
|
52
|
+
items: string().required(),
|
|
53
|
+
}, { label: "Sales" }).auth();
|
|
54
|
+
|
|
55
|
+
await start("./shop.db");
|
|
56
|
+
|
|
57
|
+
const db = getDb()!;
|
|
58
|
+
if (db.findAll("products").length === 0) {
|
|
59
|
+
db.insert("products", { name: "Coffee", price: 5 });
|
|
60
|
+
db.insert("products", { name: "Tea", price: 3 });
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Cashier page: http://localhost:3333/cashier
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Warehouse (minimal)
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { start, resource, string, number, belongsTo } from "./packages/core/src/index.js";
|
|
72
|
+
|
|
73
|
+
resource("items", {
|
|
74
|
+
name: string().required(),
|
|
75
|
+
sku: string().required(),
|
|
76
|
+
qty: number().required(),
|
|
77
|
+
}, { label: "Items" }).admin();
|
|
78
|
+
|
|
79
|
+
resource("movements", {
|
|
80
|
+
item: belongsTo("items").required(),
|
|
81
|
+
type: string().required(),
|
|
82
|
+
qty: number().required(),
|
|
83
|
+
}, { label: "Movements" }).admin().auth();
|
|
84
|
+
|
|
85
|
+
await start("./warehouse.db");
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## CRM (minimal)
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { start, resource, string, email, money, belongsTo } from "./packages/core/src/index.js";
|
|
94
|
+
|
|
95
|
+
resource("leads", {
|
|
96
|
+
name: string().required(),
|
|
97
|
+
email: email().required(),
|
|
98
|
+
phone: string().required(),
|
|
99
|
+
}, { label: "Leads" }).admin();
|
|
100
|
+
|
|
101
|
+
resource("deals", {
|
|
102
|
+
lead: belongsTo("leads").required(),
|
|
103
|
+
amount: money().required(),
|
|
104
|
+
stage: string().required(),
|
|
105
|
+
}, { label: "Deals" }).admin().auth();
|
|
106
|
+
|
|
107
|
+
resource("tasks", {
|
|
108
|
+
deal: belongsTo("deals").required(),
|
|
109
|
+
note: string().required(),
|
|
110
|
+
done: boolean(),
|
|
111
|
+
}, { label: "Tasks" }).admin().auth();
|
|
112
|
+
|
|
113
|
+
await start("./crm.db");
|
|
114
|
+
```
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Nexa Framework Reference
|
|
2
|
+
|
|
3
|
+
## Import
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import {
|
|
7
|
+
start,
|
|
8
|
+
resource,
|
|
9
|
+
string,
|
|
10
|
+
number,
|
|
11
|
+
boolean,
|
|
12
|
+
text,
|
|
13
|
+
email,
|
|
14
|
+
money,
|
|
15
|
+
belongsTo,
|
|
16
|
+
hasMany,
|
|
17
|
+
file,
|
|
18
|
+
image,
|
|
19
|
+
on,
|
|
20
|
+
dispatch,
|
|
21
|
+
getDb,
|
|
22
|
+
} from "./packages/core/src/index.js";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Commands (Vocabulary)
|
|
28
|
+
|
|
29
|
+
### `resource(name, fields, options?)`
|
|
30
|
+
|
|
31
|
+
Defines a database table + REST API.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
resource("clients", {
|
|
35
|
+
name: string().required(),
|
|
36
|
+
phone: string().required(),
|
|
37
|
+
email: email(),
|
|
38
|
+
}, { label: "Clients" });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Parameter | Type | Description |
|
|
42
|
+
|-----------|------|-------------|
|
|
43
|
+
| `name` | string | Table name (plural, lowercase). API: `/api/clients` |
|
|
44
|
+
| `fields` | object | Field builders (see below) |
|
|
45
|
+
| `options.label` | string | Display name in admin UI |
|
|
46
|
+
|
|
47
|
+
**Auto-generated:**
|
|
48
|
+
- `GET /api/{name}` — list
|
|
49
|
+
- `GET /api/{name}/:id` — get one
|
|
50
|
+
- `POST /api/{name}` — create
|
|
51
|
+
- `PUT /api/{name}/:id` — update
|
|
52
|
+
- `DELETE /api/{name}/:id` — delete
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### Field types
|
|
57
|
+
|
|
58
|
+
| Function | Type | SQL | Example |
|
|
59
|
+
|----------|------|-----|---------|
|
|
60
|
+
| `string()` | text | TEXT | name, phone, status |
|
|
61
|
+
| `number()` | number | REAL | age, qty, stock |
|
|
62
|
+
| `money()` | money | REAL | price, total, salary |
|
|
63
|
+
| `email()` | email | TEXT | email (validated) |
|
|
64
|
+
| `text()` | long text | TEXT | notes, description |
|
|
65
|
+
| `boolean()` | boolean | INTEGER | active, paid |
|
|
66
|
+
| `belongsTo("table")` | relation | INTEGER | foreign key → `{name}_id` |
|
|
67
|
+
| `hasMany("table")` | hasMany | — | loads related records (not stored) |
|
|
68
|
+
| `file()` | file | TEXT | stores upload URL |
|
|
69
|
+
| `image()` | image | TEXT | stores image upload URL |
|
|
70
|
+
|
|
71
|
+
### Field modifiers
|
|
72
|
+
|
|
73
|
+
| Modifier | Description |
|
|
74
|
+
|----------|-------------|
|
|
75
|
+
| `.required()` | Field is mandatory |
|
|
76
|
+
| `.min(n)` | Minimum value (number) or length (string) |
|
|
77
|
+
| `.max(n)` | Maximum value (number) or length (string) |
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
age: number().required().min(1).max(100)
|
|
81
|
+
name: string().required().min(2)
|
|
82
|
+
price: money().required()
|
|
83
|
+
client: belongsTo("clients").required()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Chain methods
|
|
89
|
+
|
|
90
|
+
| Method | Description |
|
|
91
|
+
|--------|-------------|
|
|
92
|
+
| `.admin()` | Show in admin UI at `/admin` |
|
|
93
|
+
| `.auth(role?)` | Require login. Default role: `"admin"`. Admin role bypasses all checks |
|
|
94
|
+
| `.policy(rules)` | Per-action authorization: `view`, `create`, `update`, `delete` |
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
resource("clients", { ... }).admin(); // public CRUD + admin page
|
|
98
|
+
resource("orders", { ... }).admin().auth(); // login required
|
|
99
|
+
resource("employees", { ... }).admin().auth("admin"); // admin only
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### `await start(dbPath, port?)`
|
|
105
|
+
|
|
106
|
+
Starts everything:
|
|
107
|
+
1. Connects to database (SQLite, PostgreSQL, or MySQL/MariaDB)
|
|
108
|
+
2. Creates all tables
|
|
109
|
+
3. Sets up auth (login/register) + job queue
|
|
110
|
+
4. Seeds admin user
|
|
111
|
+
5. Starts HTTP server
|
|
112
|
+
6. Serves admin UI from `public/`
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
await start("./app.db"); // default port 3333
|
|
116
|
+
await start("./app.db", 8080); // custom port
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Default admin:** `admin@nexa.dev` / `123456`
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## API Endpoints
|
|
124
|
+
|
|
125
|
+
### Auth
|
|
126
|
+
|
|
127
|
+
| Method | URL | Body | Description |
|
|
128
|
+
|--------|-----|------|-------------|
|
|
129
|
+
| POST | `/api/auth/login` | `{ email, password }` | Returns `{ user, token }` |
|
|
130
|
+
| POST | `/api/auth/register` | `{ email, password }` | Create user |
|
|
131
|
+
|
|
132
|
+
Use token: `Authorization: Bearer {token}`
|
|
133
|
+
|
|
134
|
+
### Resources
|
|
135
|
+
|
|
136
|
+
All resources follow REST pattern at `/api/{name}`.
|
|
137
|
+
|
|
138
|
+
Response format:
|
|
139
|
+
```json
|
|
140
|
+
{ "data": [...], "meta": { "page": 1, "limit": 20 } }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Error format:
|
|
144
|
+
```json
|
|
145
|
+
{ "error": "message", "errors": [{ "field": "name", "message": "..." }] }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Framework
|
|
149
|
+
|
|
150
|
+
| Method | URL | Description |
|
|
151
|
+
|--------|-----|------|-------------|
|
|
152
|
+
| GET | `/api` | List all endpoints |
|
|
153
|
+
| GET | `/api/schema` | Resource schemas (for admin UI) |
|
|
154
|
+
| GET | `/api/dashboard` | Stats + revenue |
|
|
155
|
+
|
|
156
|
+
### Pages
|
|
157
|
+
|
|
158
|
+
| URL | Description |
|
|
159
|
+
|-----|-------------|
|
|
160
|
+
| `/admin` | Auto-generated admin panel |
|
|
161
|
+
| `/cashier` | Cashier POS (if exists) |
|
|
162
|
+
| `/` | Redirects to admin |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Terminal
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
bun run dev # start app.ts
|
|
170
|
+
bun run our make clients # create resource file template
|
|
171
|
+
bun run our test # run tests
|
|
172
|
+
bun run our migrate # create tables only
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Relations
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
resource("clients", { name: string().required() }, { label: "Clients" }).admin();
|
|
181
|
+
|
|
182
|
+
resource("orders", {
|
|
183
|
+
client: belongsTo("clients").required(),
|
|
184
|
+
product: belongsTo("products").required(),
|
|
185
|
+
qty: number().required(),
|
|
186
|
+
total: money().required(),
|
|
187
|
+
}, { label: "Orders" }).admin().auth();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- Creates column `client_id` referencing `clients.id`
|
|
191
|
+
- API accepts `{ client: 1 }` or `{ client_id: 1 }`
|
|
192
|
+
- GET responses include nested object: `{ client: { id: 1, name: "..." } }`
|
|
193
|
+
- Admin UI shows dropdown for relation fields
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Seed data (optional)
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
await start("./app.db");
|
|
201
|
+
|
|
202
|
+
const db = getDb()!;
|
|
203
|
+
if (db.findAll("clients").length === 0) {
|
|
204
|
+
db.insert("clients", { name: "Al Noor Co", phone: "0501234567" });
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## What NOT to do
|
|
211
|
+
|
|
212
|
+
- Do NOT write Express/Fastify/NestJS routes
|
|
213
|
+
- Do NOT write SQL manually
|
|
214
|
+
- Do NOT write controllers or models
|
|
215
|
+
- Do NOT install Laravel or other frameworks
|
|
216
|
+
- Do NOT edit files in `packages/` unless extending the framework
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## What TO do
|
|
221
|
+
|
|
222
|
+
1. Edit `app.ts`
|
|
223
|
+
2. Define `resource()` for each module
|
|
224
|
+
3. Chain `.admin()` and `.auth()` as needed
|
|
225
|
+
4. Call `await start("./my.db")`
|
|
226
|
+
5. Open `/admin`
|
package/docs/LANGUAGE.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Localization (English + Arabic)
|
|
2
|
+
|
|
3
|
+
Default language is **English**.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
lang/
|
|
7
|
+
en.json ← default
|
|
8
|
+
ar.json ← Arabic (optional)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Labels
|
|
12
|
+
|
|
13
|
+
Prefer `labelKey` for shared translations, or `.label("…")` for a fixed string:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
resource("clients", {
|
|
17
|
+
name: string().required().label("Name"),
|
|
18
|
+
}, { label: "Clients", labelKey: "clients" }).admin();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
// lang/en.json
|
|
23
|
+
{ "clients": "Clients", "ui": { "login": "Login" } }
|
|
24
|
+
|
|
25
|
+
// lang/ar.json
|
|
26
|
+
{ "clients": "العملاء", "ui": { "login": "دخول" } }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Switching language
|
|
30
|
+
|
|
31
|
+
Admin includes **EN / عربي** buttons.
|
|
32
|
+
API: `GET /api/schema?lang=en` or `?lang=ar`
|
|
33
|
+
|
|
34
|
+
## Config
|
|
35
|
+
|
|
36
|
+
```env
|
|
37
|
+
APP_LOCALE=en
|
|
38
|
+
# APP_LOCALE=ar
|
|
39
|
+
```
|
package/docs/README.md
ADDED
package/docs/READY.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Nexa — Ready for review
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0.0
|
|
4
|
+
**Status:** Backend-first · Node.js · English default · publishable (`@nexa-stack/framework`)
|
|
5
|
+
**Tests:** `npm test`
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm add @nexa-stack/framework
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { resource, string, start, db, createClient, route } from "@nexa-stack/framework";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In this repo: `npm run serve`
|
|
18
|
+
|
|
19
|
+
## Feature map
|
|
20
|
+
|
|
21
|
+
| Area | Status | API |
|
|
22
|
+
|------|--------|-----|
|
|
23
|
+
| Resources | ✅ | `resource()` + `db.from()` |
|
|
24
|
+
| Relations | ✅ | `belongsTo` / `hasMany` / `belongsToMany` |
|
|
25
|
+
| Transactions | ✅ | `db.transaction()` |
|
|
26
|
+
| Migrations | ✅ | auto ADD COLUMN + `migrate:rollback` |
|
|
27
|
+
| REST API | ✅ | CRUD + `route()` |
|
|
28
|
+
| Auth | ✅ | JWT · roles · reset · verify |
|
|
29
|
+
| Soft deletes | ✅ | `.admin()` default |
|
|
30
|
+
| Admin UI | ✅ | `/admin` (English default; Arabic optional) |
|
|
31
|
+
| SQLite / PG / MySQL | ✅ | `.env` |
|
|
32
|
+
| Queue | ✅ | retries · claim |
|
|
33
|
+
| Notifications | ✅ | `notify` + `POST /api/notifications` |
|
|
34
|
+
| Frontend bridge | ✅ thin | `createClient()` |
|
|
35
|
+
| Dev Bar | ✅ | dev only |
|
|
36
|
+
| npm | ✅ | `@nexa-stack/framework@1.0.0` |
|
|
37
|
+
|
|
38
|
+
## Intentionally later
|
|
39
|
+
|
|
40
|
+
- Full Nexa frontend UI framework
|
|
41
|
+
- whereHas / scopes / morphs
|
|
42
|
+
- Multi-tenancy, GraphQL, WebSockets
|
|
43
|
+
- Community growth
|
|
44
|
+
|
|
45
|
+
## Review
|
|
46
|
+
|
|
47
|
+
1. `npm test`
|
|
48
|
+
2. `npm run serve` → `/admin`
|
|
49
|
+
3. Docs: [START](./START.md) · [REFERENCE](./REFERENCE.md)
|
|
50
|
+
|
|
51
|
+
Login: `admin@nexa.dev` / `123456`
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Nexa — API reference
|
|
2
|
+
|
|
3
|
+
Production backend framework for **Node.js ≥ 20**.
|
|
4
|
+
Package: `@nexa-stack/framework` · CLI: `nexa` · Default locale: **English**
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx @nexa-stack/framework new my-app
|
|
10
|
+
cd my-app && npm install && npm run serve
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- **CLI:** `nexa` (serve, new, make:resource, migrate, queue:work, …)
|
|
14
|
+
- **Import:** `from "@nexa-stack/framework"`
|
|
15
|
+
- **Requires:** Node.js ≥ 20
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Core idea
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { resource, string, money, belongsTo } from "@nexa-stack/framework";
|
|
23
|
+
|
|
24
|
+
resource("orders", {
|
|
25
|
+
client: belongsTo("clients").required().label("Client"),
|
|
26
|
+
total: money().required().label("Total"),
|
|
27
|
+
}, { label: "Orders" }).admin();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
One `resource` → table + REST API + admin UI.
|
|
31
|
+
|
|
32
|
+
| Feature | Status |
|
|
33
|
+
|---------|--------|
|
|
34
|
+
| REST CRUD | ✅ |
|
|
35
|
+
| Auth (JWT) | ✅ |
|
|
36
|
+
| Soft delete (admin default) | ✅ |
|
|
37
|
+
| Relations | belongsTo / hasMany / belongsToMany |
|
|
38
|
+
| Query builder | `db.from().where().with()` |
|
|
39
|
+
| Transactions | ✅ |
|
|
40
|
+
| Smart migrations | ADD COLUMN auto |
|
|
41
|
+
| Queue + retries | ✅ |
|
|
42
|
+
| Notifications | `notify` + HTTP API |
|
|
43
|
+
| Custom routes | `route()` |
|
|
44
|
+
| Dev Bar | dev only |
|
|
45
|
+
| i18n | `en` default · `ar` optional |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Fields
|
|
50
|
+
|
|
51
|
+
| Helper | DB | Notes |
|
|
52
|
+
|--------|-----|--------|
|
|
53
|
+
| `string()` | TEXT | |
|
|
54
|
+
| `text()` | TEXT | long text |
|
|
55
|
+
| `number()` | REAL/INT | |
|
|
56
|
+
| `boolean()` | INT | API returns **true/false** |
|
|
57
|
+
| `email()` | TEXT | validated |
|
|
58
|
+
| `money()` | REAL | |
|
|
59
|
+
| `date()` / `datetime()` | TEXT | |
|
|
60
|
+
| `select([...])` | TEXT | |
|
|
61
|
+
| `belongsTo("x")` | `x_id` | name the field `teacher`, not `teacher_id` |
|
|
62
|
+
| `hasMany("x")` | — | |
|
|
63
|
+
| `belongsToMany("x")` | pivot table | |
|
|
64
|
+
| `file()` / `image()` | TEXT | URL path |
|
|
65
|
+
|
|
66
|
+
Chain: `.required()` `.label("…")` `.min()` `.max()`
|
|
67
|
+
|
|
68
|
+
SQL reserved names (`order`, `group`, …) throw a clear error — rename (e.g. `lesson_order`).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## REST API
|
|
73
|
+
|
|
74
|
+
For `resource("products")`:
|
|
75
|
+
|
|
76
|
+
| Method | URL |
|
|
77
|
+
|--------|-----|
|
|
78
|
+
| GET | `/api/products` |
|
|
79
|
+
| GET | `/api/products/:id` |
|
|
80
|
+
| POST | `/api/products` |
|
|
81
|
+
| PUT | `/api/products/:id` |
|
|
82
|
+
| DELETE | `/api/products/:id` |
|
|
83
|
+
| POST | `/api/products/:id/restore` |
|
|
84
|
+
| GET | `/api/products/export?format=csv` |
|
|
85
|
+
|
|
86
|
+
List query: `?search=&sort=&order=&page=&limit=&trashed=only|with`
|
|
87
|
+
|
|
88
|
+
### Notifications
|
|
89
|
+
|
|
90
|
+
| Method | Path | Auth |
|
|
91
|
+
|--------|------|------|
|
|
92
|
+
| GET | `/api/notifications` | ✅ |
|
|
93
|
+
| POST | `/api/notifications` | ✅ create (calls `notify`) |
|
|
94
|
+
| POST | `/api/notifications/:id/read` | ✅ |
|
|
95
|
+
| POST | `/api/notifications/read-all` | ✅ |
|
|
96
|
+
|
|
97
|
+
Only **admin** may set `user_id` to notify another user.
|
|
98
|
+
|
|
99
|
+
### Custom routes
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { route, start } from "@nexa-stack/framework";
|
|
103
|
+
|
|
104
|
+
route("POST", "/api/checkout", async (req, user) => {
|
|
105
|
+
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
106
|
+
return Response.json({ data: { ok: true } });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await start();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Query builder
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { db } from "@nexa-stack/framework";
|
|
118
|
+
|
|
119
|
+
await db.from("orders").where("status", "paid").with("client").get();
|
|
120
|
+
await db.transaction(async (tx) => { /* ... */ });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Auth
|
|
126
|
+
|
|
127
|
+
| Endpoint | Notes |
|
|
128
|
+
|----------|--------|
|
|
129
|
+
| POST `/api/auth/login` | → `{ data: { user, token } }` |
|
|
130
|
+
| POST `/api/auth/register` | off in production unless `AUTH_REGISTER=true` |
|
|
131
|
+
| POST `/api/auth/forgot-password` | |
|
|
132
|
+
| POST `/api/auth/reset-password` | |
|
|
133
|
+
| POST `/api/auth/verify-email` | |
|
|
134
|
+
|
|
135
|
+
`.admin()` implies auth. Use `.policy({ … })` for roles.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Config (`.env`)
|
|
140
|
+
|
|
141
|
+
```env
|
|
142
|
+
PORT=3333
|
|
143
|
+
APP_SECRET=change-me-in-production # ≥32 chars in production
|
|
144
|
+
APP_LOCALE=en # or ar
|
|
145
|
+
DB_PATH=./app.db
|
|
146
|
+
# DATABASE_URL=mysql://user:pass@localhost:3306/nexa
|
|
147
|
+
# DATABASE_URL=postgresql://user:pass@localhost:5432/nexa
|
|
148
|
+
AUTH_RATE_LIMIT=10
|
|
149
|
+
RATE_LIMIT=120
|
|
150
|
+
HOT_RELOAD=true # off in production
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Localization
|
|
156
|
+
|
|
157
|
+
Default UI language is **English**.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
lang/en.json
|
|
161
|
+
lang/ar.json # optional Arabic
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Admin: EN / عربي buttons. Schema: `/api/schema?lang=en`
|
|
165
|
+
|
|
166
|
+
```env
|
|
167
|
+
APP_LOCALE=en
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## CLI
|
|
173
|
+
|
|
174
|
+
| Command | |
|
|
175
|
+
|---------|---|
|
|
176
|
+
| `nexa serve` | Start (hot reload in dev) |
|
|
177
|
+
| `nexa new app` | New project |
|
|
178
|
+
| `nexa make:resource X` | File under `resources/` |
|
|
179
|
+
| `nexa migrate` | Sync tables |
|
|
180
|
+
| `nexa migrate:rollback` | |
|
|
181
|
+
| `nexa db:seed` | |
|
|
182
|
+
| `nexa queue:work` | |
|
|
183
|
+
| `nexa schedule:work` | |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Hot reload (dev)
|
|
188
|
+
|
|
189
|
+
Watches `app.ts` and `resources/*.ts`. Disabled when `NODE_ENV=production`.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Dev Bar (dev only)
|
|
194
|
+
|
|
195
|
+
Bottom toolbar on `/admin`, `/docs`, `/` — not shown in production.
|
|
196
|
+
|
|
197
|
+
| Endpoint | |
|
|
198
|
+
|----------|---|
|
|
199
|
+
| `GET /api/dev/status` | port, resources, recent requests |
|
|
200
|
+
| `GET /api/dev/routes` | route manifest |
|
|
201
|
+
| `GET /dev-bar.js` | toolbar script |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Admin / health
|
|
206
|
+
|
|
207
|
+
| Path | |
|
|
208
|
+
|------|---|
|
|
209
|
+
| `/admin` | Admin UI |
|
|
210
|
+
| `/docs` | Docs |
|
|
211
|
+
| `/api/schema?lang=en` | Schema + UI strings |
|
|
212
|
+
| `/health` | `{ ok: true }` |
|
|
213
|
+
| `/ready` | DB check |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Security (v0.5+)
|
|
218
|
+
|
|
219
|
+
- Signed JWT (HMAC-SHA256)
|
|
220
|
+
- Mass assignment ignored for unknown fields
|
|
221
|
+
- `.admin()` requires auth
|
|
222
|
+
- Register closed in production by default
|
|
223
|
+
- Auth rate limit
|
|
224
|
+
- Strong `APP_SECRET` required in production
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Project layout
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
app.ts
|
|
232
|
+
resources/*.ts
|
|
233
|
+
.env
|
|
234
|
+
lang/en.json
|
|
235
|
+
lang/ar.json
|
|
236
|
+
public/
|
|
237
|
+
database/migrations/
|
|
238
|
+
database/seeders/
|
|
239
|
+
schedule.ts # optional
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Checklist for testers (1.0.0)
|
|
245
|
+
|
|
246
|
+
1. `npx @nexa-stack/framework new test-app && cd test-app && npm install`
|
|
247
|
+
2. Read project `README.md`
|
|
248
|
+
3. `npm run serve` → `/admin` (EN default) + `/docs`
|
|
249
|
+
4. `npx nexa make:resource products` → `/api/products` without restart
|
|
250
|
+
5. DELETE on `.admin()` resource → soft delete
|
|
251
|
+
6. `boolean()` JSON is `true`/`false`
|
|
252
|
+
7. Failed logins → 429 after threshold
|
|
253
|
+
8. Port in use → auto next port
|
|
254
|
+
9. Production without strong `APP_SECRET` → refuses to start
|
|
255
|
+
10. `package.json` has `"@nexa-stack/framework": "^1.0.x"`
|