@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/database.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Database
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## SQLite (default)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
```env
|
|
10
|
+
|
|
11
|
+
DB_PATH=./app.db
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## PostgreSQL
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
```env
|
|
22
|
+
|
|
23
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/nexa
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
When `DATABASE_URL` is set, Nexa picks the driver from the URL scheme. Otherwise SQLite via `DB_PATH`.
|
|
30
|
+
|
|
31
|
+
## MySQL / MariaDB
|
|
32
|
+
|
|
33
|
+
```env
|
|
34
|
+
DATABASE_URL=mysql://user:pass@localhost:3306/nexa
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`mariadb://` URLs work the same way.
|
|
38
|
+
|
|
39
|
+
| Driver | URL prefix | Default env |
|
|
40
|
+
|--------|------------|-------------|
|
|
41
|
+
| SQLite | file path | `DB_PATH=./app.db` |
|
|
42
|
+
| PostgreSQL | `postgresql://` | `DATABASE_URL` |
|
|
43
|
+
| MySQL / MariaDB | `mysql://` or `mariadb://` | `DATABASE_URL` |
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Query from code
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
|
|
53
|
+
const db = getDb()!;
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// All
|
|
58
|
+
|
|
59
|
+
await db.findAll("clients");
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
// One
|
|
64
|
+
|
|
65
|
+
await db.findById("clients", 1);
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// Insert
|
|
70
|
+
|
|
71
|
+
await db.insert("clients", { name: "Ali", phone: "050..." });
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// SQL
|
|
76
|
+
|
|
77
|
+
await db.query("SELECT * FROM clients WHERE city = ?", ["Damascus"]);
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Relations
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
### belongsTo
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
|
|
93
|
+
resource("orders", {
|
|
94
|
+
|
|
95
|
+
client: belongsTo("clients").required(),
|
|
96
|
+
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
API returns nested object: `{ client: { id: 1, name: "Ali" } }`
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
### hasMany
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
|
|
113
|
+
resource("clients", {
|
|
114
|
+
|
|
115
|
+
name: string().required(),
|
|
116
|
+
|
|
117
|
+
orders: hasMany("orders"),
|
|
118
|
+
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
API returns related records: `{ orders: [{ id: 1, ... }] }`
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Foreign key on child table: `client_id` (auto from parent table name).
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## File uploads
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
Field types:
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
|
|
143
|
+
resource("products", {
|
|
144
|
+
|
|
145
|
+
name: string().required(),
|
|
146
|
+
|
|
147
|
+
photo: image(),
|
|
148
|
+
|
|
149
|
+
manual: file(),
|
|
150
|
+
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
Upload endpoint (requires login):
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
POST /api/upload
|
|
164
|
+
|
|
165
|
+
Content-Type: multipart/form-data
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
file=<binary>
|
|
170
|
+
|
|
171
|
+
type=image # optional, validates image extensions
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
Response:
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
|
|
183
|
+
{ "data": { "url": "/storage/uploads/uuid.jpg", "name": "photo.jpg" } }
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
Save the `url` in your resource field when creating/updating.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## Policies
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
Protect resources per action:
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
|
|
203
|
+
resource("invoices", { ... })
|
|
204
|
+
|
|
205
|
+
.policy({
|
|
206
|
+
|
|
207
|
+
view: "admin",
|
|
208
|
+
|
|
209
|
+
create: "admin",
|
|
210
|
+
|
|
211
|
+
update: (user) => user?.role === "manager",
|
|
212
|
+
|
|
213
|
+
delete: "admin",
|
|
214
|
+
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
`.auth("admin")` is shorthand for all actions requiring that role.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
## Events & Queue
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
Events fire on CRUD:
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
- `orders.created`
|
|
234
|
+
|
|
235
|
+
- `orders.updated`
|
|
236
|
+
|
|
237
|
+
- `orders.deleted`
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
Listen:
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
|
|
247
|
+
import { on } from "./packages/core/src/index.js";
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
on("orders.created", async (payload) => {
|
|
252
|
+
|
|
253
|
+
console.log("New order", payload);
|
|
254
|
+
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
Queue jobs:
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
|
|
267
|
+
import { dispatch } from "./packages/core/src/index.js";
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
await dispatch(getDb()!, "orders.created", { id: 1 });
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
Process jobs:
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
|
|
283
|
+
bun run nexa queue:work
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
## API — Search
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
GET /api/clients?search=ali
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
Searches all string/email/text fields.
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
## API — Filter
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
GET /api/clients?city=Damascus
|
|
312
|
+
|
|
313
|
+
GET /api/orders?status=paid
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
## API — Sort
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
GET /api/clients?sort=name&order=asc
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
## API — Pagination (Laravel format)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
GET /api/clients?page=2&limit=20
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
Response:
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
|
|
349
|
+
{
|
|
350
|
+
|
|
351
|
+
"data": [...],
|
|
352
|
+
|
|
353
|
+
"meta": {
|
|
354
|
+
|
|
355
|
+
"current_page": 2,
|
|
356
|
+
|
|
357
|
+
"last_page": 5,
|
|
358
|
+
|
|
359
|
+
"per_page": 20,
|
|
360
|
+
|
|
361
|
+
"total": 100,
|
|
362
|
+
|
|
363
|
+
"from": 21,
|
|
364
|
+
|
|
365
|
+
"to": 40
|
|
366
|
+
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
"links": {
|
|
370
|
+
|
|
371
|
+
"first": "...",
|
|
372
|
+
|
|
373
|
+
"last": "...",
|
|
374
|
+
|
|
375
|
+
"prev": "...",
|
|
376
|
+
|
|
377
|
+
"next": "..."
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
## Combine
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
GET /api/clients?search=ali&city=Damascus&sort=name&order=asc&page=1&limit=10
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- [Node.js](https://nodejs.org) ≥ 20
|
|
6
|
+
- Optional: PostgreSQL or MySQL for production
|
|
7
|
+
- Optional: Redis for `CACHE_DRIVER=redis`
|
|
8
|
+
|
|
9
|
+
## In this repo
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd F:\framework
|
|
13
|
+
npm install
|
|
14
|
+
npm run serve
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- Admin: http://localhost:3333/admin
|
|
18
|
+
- Docs: http://localhost:3333/docs
|
|
19
|
+
- API: http://localhost:3333/api
|
|
20
|
+
|
|
21
|
+
Login: `admin@nexa.dev` / `123456`
|
|
22
|
+
|
|
23
|
+
## As a package
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @nexa-stack/framework new my-shop
|
|
27
|
+
cd my-shop
|
|
28
|
+
npm install
|
|
29
|
+
npm run serve
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Language
|
|
33
|
+
|
|
34
|
+
Default is **English** (`APP_LOCALE=en`).
|
|
35
|
+
|
|
36
|
+
```env
|
|
37
|
+
APP_LOCALE=en # default
|
|
38
|
+
# APP_LOCALE=ar # Arabic UI + RTL in admin
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Hostinger
|
|
42
|
+
|
|
43
|
+
1. Create a MySQL database in hPanel
|
|
44
|
+
2. Node.js app (Node ≥ 20)
|
|
45
|
+
3. `.env`:
|
|
46
|
+
|
|
47
|
+
```env
|
|
48
|
+
DATABASE_URL=mysql://USER:PASS@localhost:3306/DBNAME
|
|
49
|
+
APP_SECRET=change-me-to-a-long-random-string
|
|
50
|
+
NODE_ENV=production
|
|
51
|
+
PORT=3333
|
|
52
|
+
APP_LOCALE=en
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
4. Start: `npx nexa serve`
|
|
56
|
+
|
|
57
|
+
SQLite uses **sql.js** (WASM) — no Visual Studio / node-gyp. Prefer MySQL in production.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Localization
|
|
2
|
+
|
|
3
|
+
Default language is **English** (`APP_LOCALE=en`).
|
|
4
|
+
|
|
5
|
+
## Resource labels
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
resource("clients", { ... }, { labelKey: "clients" }).admin();
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`lang/en.json`:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"clients": "Clients",
|
|
16
|
+
"products": "Products",
|
|
17
|
+
"ui": { "login": "Login", "add": "Add" }
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Optional `lang/ar.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"clients": "العملاء",
|
|
26
|
+
"products": "المنتجات",
|
|
27
|
+
"ui": { "login": "دخول", "add": "إضافة" }
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Admin language
|
|
32
|
+
|
|
33
|
+
EN / عربي toggle in the admin UI, or:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
GET /api/schema?lang=en
|
|
37
|
+
GET /api/schema?lang=ar
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Default locale
|
|
41
|
+
|
|
42
|
+
```env
|
|
43
|
+
APP_LOCALE=en
|
|
44
|
+
# APP_LOCALE=ar
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
See also [LANGUAGE.md](./LANGUAGE.md).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Resources
|
|
2
|
+
|
|
3
|
+
A **resource** = database table + REST API + optional admin page.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { resource, string, money, belongsTo } from "@nexa-stack/framework";
|
|
7
|
+
// or: from "./packages/core/src/index.js"
|
|
8
|
+
|
|
9
|
+
resource("products", {
|
|
10
|
+
name: string().required(),
|
|
11
|
+
price: money().required(),
|
|
12
|
+
supplier: belongsTo("suppliers"),
|
|
13
|
+
}, { labelKey: "products" })
|
|
14
|
+
.admin()
|
|
15
|
+
.auth()
|
|
16
|
+
.softDeletes();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Options
|
|
20
|
+
|
|
21
|
+
| Option | Description |
|
|
22
|
+
|--------|-------------|
|
|
23
|
+
| `label` | Display name (admin) |
|
|
24
|
+
| `labelKey` | Key in `lang/ar.json` / `lang/en.json` |
|
|
25
|
+
|
|
26
|
+
## Chain methods
|
|
27
|
+
|
|
28
|
+
| Method | Effect |
|
|
29
|
+
|--------|--------|
|
|
30
|
+
| `.admin()` | Show in `/admin` sidebar + **soft delete by default** |
|
|
31
|
+
| `.auth(role?)` | Require login (default role `admin`) |
|
|
32
|
+
| `.policy({...})` | Per-action rules: view/create/update/delete |
|
|
33
|
+
| `.softDeletes()` | Soft delete + restore (already on with `.admin()`) |
|
|
34
|
+
| `.hardDeletes()` | Permanent DELETE instead of soft |
|
|
35
|
+
|
|
36
|
+
## Field builders
|
|
37
|
+
|
|
38
|
+
| Builder | Column | Notes |
|
|
39
|
+
|---------|--------|-------|
|
|
40
|
+
| `string()` | TEXT | |
|
|
41
|
+
| `text()` | TEXT | long text |
|
|
42
|
+
| `number()` | number | |
|
|
43
|
+
| `money()` | number | money amounts |
|
|
44
|
+
| `email()` | TEXT | validated |
|
|
45
|
+
| `boolean()` | bool/int | returns **true/false** in JSON |
|
|
46
|
+
| `file()` / `image()` | TEXT | stores URL |
|
|
47
|
+
| `belongsTo("table")` | `{name}_id` | nested on GET |
|
|
48
|
+
| `hasMany("table")` | — | nested list on GET |
|
|
49
|
+
| `belongsToMany("table")` | pivot auto | e.g. `post_tag` |
|
|
50
|
+
|
|
51
|
+
Modifiers: `.required()` · `.min(n)` · `.max(n)`
|
|
52
|
+
|
|
53
|
+
## Auto API
|
|
54
|
+
|
|
55
|
+
For `resource("products", ...)`:
|
|
56
|
+
|
|
57
|
+
| Method | URL |
|
|
58
|
+
|--------|-----|
|
|
59
|
+
| GET | `/api/products` |
|
|
60
|
+
| GET | `/api/products/:id` |
|
|
61
|
+
| POST | `/api/products` |
|
|
62
|
+
| PUT | `/api/products/:id` |
|
|
63
|
+
| DELETE | `/api/products/:id` |
|
|
64
|
+
| GET | `/api/products/export?format=csv` |
|
|
65
|
+
| POST | `/api/products/:id/restore` | if softDeletes |
|
|
66
|
+
|
|
67
|
+
## Query params (list)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
?search=ali&city=Damascus&sort=name&order=asc&page=1&limit=20
|
|
71
|
+
?trashed=only # soft-deleted only
|
|
72
|
+
?trashed=with # include deleted
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
See [database.md](./database.md) for pagination response shape.
|
package/docs/routing.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
You do **not** write routes. Nexa registers them from resources.
|
|
4
|
+
|
|
5
|
+
## Built-in endpoints
|
|
6
|
+
|
|
7
|
+
### Framework
|
|
8
|
+
|
|
9
|
+
| Method | URL | Description |
|
|
10
|
+
|--------|-----|-------------|
|
|
11
|
+
| GET | `/api` | List endpoints |
|
|
12
|
+
| GET | `/api/schema?lang=ar` | Admin schema |
|
|
13
|
+
| GET | `/api/dashboard` | Stats |
|
|
14
|
+
|
|
15
|
+
### Auth
|
|
16
|
+
|
|
17
|
+
| Method | URL |
|
|
18
|
+
|--------|-----|
|
|
19
|
+
| POST | `/api/auth/register` |
|
|
20
|
+
| POST | `/api/auth/login` |
|
|
21
|
+
| POST | `/api/auth/forgot-password` |
|
|
22
|
+
| POST | `/api/auth/reset-password` |
|
|
23
|
+
| GET/POST | `/api/auth/verify-email` |
|
|
24
|
+
| POST | `/api/auth/resend-verification` |
|
|
25
|
+
|
|
26
|
+
### Other
|
|
27
|
+
|
|
28
|
+
| Method | URL |
|
|
29
|
+
|--------|-----|
|
|
30
|
+
| POST | `/api/upload` | multipart `file` |
|
|
31
|
+
| GET | `/api/notifications` | unread (auth) |
|
|
32
|
+
| POST | `/api/notifications/:id/read` | |
|
|
33
|
+
| POST | `/api/notifications/read-all` | |
|
|
34
|
+
| GET | `/storage/uploads/...` | served files |
|
|
35
|
+
|
|
36
|
+
### Pages
|
|
37
|
+
|
|
38
|
+
| URL | Page |
|
|
39
|
+
|-----|------|
|
|
40
|
+
| `/admin` | Admin UI |
|
|
41
|
+
| `/docs` | Documentation |
|
|
42
|
+
| `/cashier` | Demo (if present) |
|
|
43
|
+
|
|
44
|
+
## Custom middleware
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { use } from "@nexa-stack/framework";
|
|
48
|
+
|
|
49
|
+
use(async (req, user, next) => {
|
|
50
|
+
// before
|
|
51
|
+
const res = await next();
|
|
52
|
+
// after
|
|
53
|
+
return res;
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Default on `start()`: CORS + rate limit.
|
|
58
|
+
|
|
59
|
+
See [advanced.md](./advanced.md).
|
package/docs/seeding.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Seeding
|
|
2
|
+
|
|
3
|
+
Put seeders in `database/seeders/*.ts`.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// database/seeders/erp.ts
|
|
7
|
+
import type { Database } from "../../packages/database/src/database.js";
|
|
8
|
+
|
|
9
|
+
export default async function seed(db: Database) {
|
|
10
|
+
if ((await db.findAll("clients")).length > 0) return;
|
|
11
|
+
|
|
12
|
+
await db.insert("clients", {
|
|
13
|
+
name: "Al Noor",
|
|
14
|
+
phone: "0502222222",
|
|
15
|
+
email: "n@corp.com",
|
|
16
|
+
city: "Damascus",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun run nexa db:seed
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Also runs automatically on `nexa serve` after migrations.
|
|
28
|
+
|
|
29
|
+
## Tips
|
|
30
|
+
|
|
31
|
+
- Check if data exists before inserting (idempotent)
|
|
32
|
+
- Seed order: parents before children (suppliers → products → orders)
|
|
33
|
+
- Default admin user is created by the framework (`admin@nexa.dev`)
|