@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/.env.example
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Nexa
|
|
2
|
+
DB_PATH=./app.db
|
|
3
|
+
APP_URL=http://localhost:3333
|
|
4
|
+
|
|
5
|
+
# PostgreSQL
|
|
6
|
+
# DATABASE_URL=postgresql://user:pass@localhost:5432/nexa
|
|
7
|
+
|
|
8
|
+
# MySQL / MariaDB
|
|
9
|
+
# DATABASE_URL=mysql://user:pass@localhost:3306/nexa
|
|
10
|
+
|
|
11
|
+
PORT=3333
|
|
12
|
+
# REQUIRED in production — use a long random string
|
|
13
|
+
APP_SECRET=change-me-in-production
|
|
14
|
+
APP_LOCALE=en
|
|
15
|
+
|
|
16
|
+
# Optional admin seed (created only if no admin exists)
|
|
17
|
+
# ADMIN_EMAIL=admin@nexa.dev
|
|
18
|
+
# ADMIN_PASSWORD=123456
|
|
19
|
+
|
|
20
|
+
# Mail: log (dev) | smtp
|
|
21
|
+
MAIL_DRIVER=log
|
|
22
|
+
MAIL_FROM=nexa@localhost
|
|
23
|
+
|
|
24
|
+
# Cache: memory | file | redis
|
|
25
|
+
CACHE_DRIVER=memory
|
|
26
|
+
|
|
27
|
+
# Uploads (bytes) — default 5MB
|
|
28
|
+
UPLOAD_MAX_BYTES=5000000
|
|
29
|
+
|
|
30
|
+
# Rate limit: memory (default) | redis (multi-instance; needs CACHE_DRIVER=redis or RATE_LIMIT_DRIVER=redis)
|
|
31
|
+
# RATE_LIMIT_DRIVER=redis
|
|
32
|
+
# REDIS_URL=redis://127.0.0.1:6379
|
|
33
|
+
|
|
34
|
+
# List API cache TTL in seconds (0 = off). Speeds read-heavy apps behind multiple instances when using redis cache.
|
|
35
|
+
# LIST_CACHE_TTL=15
|
|
36
|
+
|
|
37
|
+
# Optional: allow public sign-up (default: on in dev, off when NODE_ENV=production)
|
|
38
|
+
# AUTH_REGISTER=true
|
|
39
|
+
|
|
40
|
+
# Auth brute-force limit (POST /api/auth/* per IP per minute)
|
|
41
|
+
# AUTH_RATE_LIMIT=10
|
|
42
|
+
|
|
43
|
+
# Hot reload (dev only — off in NODE_ENV=production)
|
|
44
|
+
# HOT_RELOAD=false
|
|
45
|
+
|
|
46
|
+
LOG_LEVEL=debug
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nexa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Nexa
|
|
2
|
+
|
|
3
|
+
Write little → ship a full backend. Goal: **hours, not weeks.**
|
|
4
|
+
|
|
5
|
+
Runs on **Node.js ≥ 20** (Hostinger-friendly). Default UI language is **English**.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @nexa-stack/framework new my-app
|
|
11
|
+
cd my-app
|
|
12
|
+
npm install
|
|
13
|
+
npm run serve
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
→ http://localhost:3333
|
|
17
|
+
→ http://localhost:3333/admin
|
|
18
|
+
→ Login: `admin@nexa.dev` / `123456`
|
|
19
|
+
|
|
20
|
+
## The idea
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { resource, string, money, belongsTo, select } from "@nexa-stack/framework";
|
|
24
|
+
|
|
25
|
+
resource("orders", {
|
|
26
|
+
client: belongsTo("clients").required().label("Client"),
|
|
27
|
+
total: money().required().label("Total"),
|
|
28
|
+
status: select(["new", "paid"]).label("Status"),
|
|
29
|
+
}, { label: "Orders" }).admin();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That gives you: database table + REST API + admin panel.
|
|
33
|
+
|
|
34
|
+
Optional power tools:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
await db.from("orders").where("status", "paid").with("client").get();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | What it does |
|
|
43
|
+
|---------|----------------|
|
|
44
|
+
| `nexa serve` | Start server |
|
|
45
|
+
| `nexa new X` | Scaffold a project |
|
|
46
|
+
| `nexa make:resource X` | Add `resources/X.ts` (auto-loaded) |
|
|
47
|
+
|
|
48
|
+
## Localization
|
|
49
|
+
|
|
50
|
+
- Default: `APP_LOCALE=en`
|
|
51
|
+
- Arabic UI: set `APP_LOCALE=ar` and use `lang/ar.json`
|
|
52
|
+
- Admin has EN / عربي switch either way
|
|
53
|
+
|
|
54
|
+
## Hostinger / production
|
|
55
|
+
|
|
56
|
+
Prefer MySQL on shared hosting:
|
|
57
|
+
|
|
58
|
+
```env
|
|
59
|
+
DATABASE_URL=mysql://user:pass@localhost:3306/dbname
|
|
60
|
+
APP_SECRET=long-random-secret-at-least-32-chars
|
|
61
|
+
NODE_ENV=production
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
SQLite uses `sql.js` (no Visual Studio / node-gyp).
|
|
65
|
+
|
|
66
|
+
## Docs
|
|
67
|
+
|
|
68
|
+
- [docs/START.md](./docs/START.md) — start here
|
|
69
|
+
- [docs/REFERENCE.md](./docs/REFERENCE.md) — full API reference
|
|
70
|
+
- After `nexa serve`: http://localhost:3333/docs
|
|
71
|
+
|
|
72
|
+
Package: [`@nexa-stack/framework`](https://www.npmjs.com/package/@nexa-stack/framework)
|
package/bin/nexa.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Resolve tsx from this package (works with npx / bunx / local install).
|
|
4
|
+
* Avoids bare `--import tsx` which resolves from an empty cwd cache.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const pkgRoot = join(dir, "..");
|
|
13
|
+
const require = createRequire(join(pkgRoot, "package.json"));
|
|
14
|
+
|
|
15
|
+
let tsxLoader;
|
|
16
|
+
try {
|
|
17
|
+
tsxLoader = require.resolve("tsx/esm");
|
|
18
|
+
} catch {
|
|
19
|
+
try {
|
|
20
|
+
tsxLoader = require.resolve("tsx");
|
|
21
|
+
} catch {
|
|
22
|
+
console.error(
|
|
23
|
+
"[nexa] Missing dependency `tsx`.\n" +
|
|
24
|
+
" Fix: npm install @nexa-stack/framework\n" +
|
|
25
|
+
" Or: cd your-app && npm install"
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = spawnSync(
|
|
32
|
+
process.execPath,
|
|
33
|
+
[
|
|
34
|
+
"--import",
|
|
35
|
+
pathToFileURL(tsxLoader).href,
|
|
36
|
+
join(dir, "nexa.ts"),
|
|
37
|
+
...process.argv.slice(2),
|
|
38
|
+
],
|
|
39
|
+
{ stdio: "inherit", env: process.env, cwd: process.cwd() }
|
|
40
|
+
);
|
|
41
|
+
process.exit(result.status ?? 1);
|
package/bin/nexa.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, existsSync, cpSync, writeFileSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { moduleDir } from "../packages/core/src/runtime.js";
|
|
6
|
+
|
|
7
|
+
const here = moduleDir(import.meta.url);
|
|
8
|
+
const frameworkRoot = join(here, "..").replace(/\\/g, "/");
|
|
9
|
+
const frameworkPkg = JSON.parse(readFileSync(join(here, "../package.json"), "utf8"));
|
|
10
|
+
const PKG = frameworkPkg.name as string;
|
|
11
|
+
const PKG_VERSION = frameworkPkg.version as string;
|
|
12
|
+
|
|
13
|
+
const [command, ...args] = process.argv.slice(2);
|
|
14
|
+
const arg = args[0];
|
|
15
|
+
|
|
16
|
+
const help = `
|
|
17
|
+
Nexa — ship a full backend in hours
|
|
18
|
+
|
|
19
|
+
nexa serve Start the server
|
|
20
|
+
nexa new my-app Create a new project
|
|
21
|
+
nexa make:resource X New resource file (auto-loaded)
|
|
22
|
+
nexa migrate Sync database tables
|
|
23
|
+
nexa migrate:rollback Roll back last file migration
|
|
24
|
+
nexa db:seed Run seeders
|
|
25
|
+
nexa queue:work Queue worker (scale)
|
|
26
|
+
nexa queue:work once Process one batch
|
|
27
|
+
nexa schedule:work Run scheduled tasks
|
|
28
|
+
nexa test Run tests
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const cwd = process.cwd().replace(/\\/g, "/");
|
|
32
|
+
const appUrl = new URL(`file:///${cwd}/app.ts`).href;
|
|
33
|
+
|
|
34
|
+
async function loadApp() {
|
|
35
|
+
await import(appUrl);
|
|
36
|
+
const { loadResources } = await import("../packages/core/src/loader.js");
|
|
37
|
+
await loadResources(cwd);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function withDb() {
|
|
41
|
+
await loadApp();
|
|
42
|
+
const { runMigrations } = await import("../packages/core/src/migrate.js");
|
|
43
|
+
const { loadEnv } = await import("../packages/core/src/config.js");
|
|
44
|
+
const { dbConnection } = await import("../packages/database/src/database.js");
|
|
45
|
+
loadEnv();
|
|
46
|
+
return runMigrations(dbConnection());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (command === "serve" || command === "run") {
|
|
50
|
+
await loadApp();
|
|
51
|
+
const { start } = await import("../packages/core/src/app.js");
|
|
52
|
+
await start();
|
|
53
|
+
} else if (command === "new" && arg) {
|
|
54
|
+
const name = arg.toLowerCase().replace(/[^a-z0-9-_]/g, "-");
|
|
55
|
+
const dir = join(process.cwd(), name);
|
|
56
|
+
if (existsSync(dir)) {
|
|
57
|
+
console.error(`Exists: ${dir}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
mkdirSync(join(dir, "public"), { recursive: true });
|
|
61
|
+
mkdirSync(join(dir, "resources"), { recursive: true });
|
|
62
|
+
mkdirSync(join(dir, "lang"), { recursive: true });
|
|
63
|
+
mkdirSync(join(dir, "database", "seeders"), { recursive: true });
|
|
64
|
+
const frameworkPath = frameworkRoot;
|
|
65
|
+
const isPublishedInstall = here.replace(/\\/g, "/").includes("/node_modules/");
|
|
66
|
+
const depVersion = isPublishedInstall ? `^${PKG_VERSION}` : `file:${frameworkPath}`;
|
|
67
|
+
writeFileSync(
|
|
68
|
+
join(dir, "package.json"),
|
|
69
|
+
JSON.stringify(
|
|
70
|
+
{
|
|
71
|
+
name,
|
|
72
|
+
private: true,
|
|
73
|
+
type: "module",
|
|
74
|
+
scripts: {
|
|
75
|
+
serve: "nexa serve",
|
|
76
|
+
start: "nexa serve",
|
|
77
|
+
nexa: "nexa",
|
|
78
|
+
},
|
|
79
|
+
dependencies: { [PKG]: depVersion },
|
|
80
|
+
engines: { node: ">=20" },
|
|
81
|
+
},
|
|
82
|
+
null,
|
|
83
|
+
2
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
writeFileSync(
|
|
87
|
+
join(dir, "app.ts"),
|
|
88
|
+
`import { resource, string, text, boolean } from "${PKG}";
|
|
89
|
+
|
|
90
|
+
// Prefer extra tables in resources/*.ts (auto-loaded)
|
|
91
|
+
resource("posts", {
|
|
92
|
+
title: string().required().label("Title"),
|
|
93
|
+
body: text().label("Body"),
|
|
94
|
+
published: boolean().label("Published"),
|
|
95
|
+
}, { label: "Posts" }).admin();
|
|
96
|
+
`
|
|
97
|
+
);
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(dir, ".env"),
|
|
100
|
+
`DB_PATH=./app.db
|
|
101
|
+
PORT=3333
|
|
102
|
+
APP_SECRET=change-me-in-dev-only
|
|
103
|
+
APP_LOCALE=en
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
writeFileSync(
|
|
107
|
+
join(dir, ".gitignore"),
|
|
108
|
+
`node_modules/
|
|
109
|
+
.env
|
|
110
|
+
*.db
|
|
111
|
+
*.db-shm
|
|
112
|
+
*.db-wal
|
|
113
|
+
storage/
|
|
114
|
+
`
|
|
115
|
+
);
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(dir, "README.md"),
|
|
118
|
+
`# ${name}
|
|
119
|
+
|
|
120
|
+
Nexa app — quick start.
|
|
121
|
+
|
|
122
|
+
## Run
|
|
123
|
+
|
|
124
|
+
\`\`\`bash
|
|
125
|
+
npm install
|
|
126
|
+
npm run serve
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
| URL | |
|
|
130
|
+
|-----|---|
|
|
131
|
+
| Home | http://localhost:3333 |
|
|
132
|
+
| Admin | http://localhost:3333/admin |
|
|
133
|
+
| Docs | http://localhost:3333/docs |
|
|
134
|
+
|
|
135
|
+
**Admin login:** \`admin@nexa.dev\` / \`123456\`
|
|
136
|
+
|
|
137
|
+
Default UI language is **English**. For Arabic set \`APP_LOCALE=ar\`.
|
|
138
|
+
|
|
139
|
+
## New table
|
|
140
|
+
|
|
141
|
+
\`\`\`bash
|
|
142
|
+
npx nexa make:resource products
|
|
143
|
+
npx nexa migrate
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
Files in \`resources/*.ts\` load automatically (hot reload in development).
|
|
147
|
+
|
|
148
|
+
## Example resource
|
|
149
|
+
|
|
150
|
+
\`\`\`ts
|
|
151
|
+
import { resource, string, money, belongsTo } from "${PKG}";
|
|
152
|
+
|
|
153
|
+
resource("orders", {
|
|
154
|
+
client: belongsTo("clients").required().label("Client"),
|
|
155
|
+
total: money().required(),
|
|
156
|
+
}, { label: "Orders" }).admin();
|
|
157
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
## Hostinger / production
|
|
160
|
+
|
|
161
|
+
Prefer MySQL on shared hosting:
|
|
162
|
+
|
|
163
|
+
\`\`\`
|
|
164
|
+
DATABASE_URL=mysql://user:pass@localhost:3306/dbname
|
|
165
|
+
APP_SECRET=use-a-long-random-secret
|
|
166
|
+
NODE_ENV=production
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
\`.admin()\` enables API + admin UI + auth + soft deletes by default.
|
|
170
|
+
|
|
171
|
+
## Full API reference
|
|
172
|
+
|
|
173
|
+
- After install: \`node_modules/${PKG}/docs/REFERENCE.md\`
|
|
174
|
+
- Or http://localhost:3333/docs
|
|
175
|
+
|
|
176
|
+
## Production checklist
|
|
177
|
+
|
|
178
|
+
\`\`\`env
|
|
179
|
+
NODE_ENV=production
|
|
180
|
+
APP_SECRET=random-string-at-least-32-chars
|
|
181
|
+
# AUTH_REGISTER=true # only if you want public sign-up
|
|
182
|
+
\`\`\`
|
|
183
|
+
|
|
184
|
+
## Commands
|
|
185
|
+
|
|
186
|
+
| Command | |
|
|
187
|
+
|---------|---|
|
|
188
|
+
| \`npm run serve\` | Start server |
|
|
189
|
+
| \`npx nexa make:resource X\` | New resource |
|
|
190
|
+
| \`npx nexa migrate\` | Sync DB |
|
|
191
|
+
| \`npx nexa queue:work\` | Queue worker |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
@${PKG} ${PKG_VERSION} · https://www.npmjs.com/package/${PKG}
|
|
195
|
+
`
|
|
196
|
+
);
|
|
197
|
+
writeFileSync(
|
|
198
|
+
join(dir, "lang/en.json"),
|
|
199
|
+
JSON.stringify(
|
|
200
|
+
{
|
|
201
|
+
posts: "Posts",
|
|
202
|
+
ui: {
|
|
203
|
+
title: "Nexa",
|
|
204
|
+
dashboard: "Dashboard",
|
|
205
|
+
login: "Login",
|
|
206
|
+
logout: "Logout",
|
|
207
|
+
add: "Add",
|
|
208
|
+
delete: "Delete",
|
|
209
|
+
empty: "No data",
|
|
210
|
+
confirm_delete: "Delete?",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
null,
|
|
214
|
+
2
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
writeFileSync(
|
|
218
|
+
join(dir, "lang/ar.json"),
|
|
219
|
+
JSON.stringify(
|
|
220
|
+
{
|
|
221
|
+
posts: "المنشورات",
|
|
222
|
+
ui: {
|
|
223
|
+
title: "Nexa",
|
|
224
|
+
dashboard: "لوحة التحكم",
|
|
225
|
+
login: "دخول",
|
|
226
|
+
logout: "خروج",
|
|
227
|
+
add: "إضافة",
|
|
228
|
+
delete: "حذف",
|
|
229
|
+
empty: "لا يوجد بيانات",
|
|
230
|
+
confirm_delete: "حذف؟",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
null,
|
|
234
|
+
2
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
const adminSrc = join(frameworkPath, "public/admin.html");
|
|
238
|
+
if (existsSync(adminSrc)) {
|
|
239
|
+
cpSync(adminSrc, join(dir, "public/admin.html"));
|
|
240
|
+
}
|
|
241
|
+
const indexSrc = join(frameworkPath, "public/index.html");
|
|
242
|
+
if (existsSync(indexSrc)) {
|
|
243
|
+
cpSync(indexSrc, join(dir, "public/index.html"));
|
|
244
|
+
} else {
|
|
245
|
+
writeFileSync(
|
|
246
|
+
join(dir, "public/index.html"),
|
|
247
|
+
`<!DOCTYPE html><meta http-equiv="refresh" content="0;url=/admin"><a href="/admin">Admin</a>`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
console.log(`
|
|
251
|
+
Project ready: ${name}
|
|
252
|
+
|
|
253
|
+
cd ${name}
|
|
254
|
+
npm install
|
|
255
|
+
npm run serve
|
|
256
|
+
|
|
257
|
+
Read README.md for the full quickstart
|
|
258
|
+
Open http://localhost:3333/admin
|
|
259
|
+
Login: admin@nexa.dev / 123456
|
|
260
|
+
|
|
261
|
+
New table:
|
|
262
|
+
npx nexa make:resource products
|
|
263
|
+
`);
|
|
264
|
+
} else if (command === "migrate") {
|
|
265
|
+
await withDb();
|
|
266
|
+
} else if (command === "migrate:rollback") {
|
|
267
|
+
await loadApp();
|
|
268
|
+
const { runMigrations, rollbackMigrations } = await import("../packages/core/src/migrate.js");
|
|
269
|
+
const { loadEnv } = await import("../packages/core/src/config.js");
|
|
270
|
+
const { dbConnection } = await import("../packages/database/src/database.js");
|
|
271
|
+
loadEnv();
|
|
272
|
+
const database = await runMigrations(dbConnection());
|
|
273
|
+
const n = await rollbackMigrations(database, Number(arg) || 1);
|
|
274
|
+
console.log(`Rolled back ${n} migration(s)`);
|
|
275
|
+
} else if (command === "db:seed") {
|
|
276
|
+
const db = await withDb();
|
|
277
|
+
const { runSeeders } = await import("../packages/core/src/migrate.js");
|
|
278
|
+
await runSeeders(db);
|
|
279
|
+
console.log("Seed complete");
|
|
280
|
+
} else if (command === "queue:work") {
|
|
281
|
+
const database = await withDb();
|
|
282
|
+
const { setupQueue, processJobs, workQueue, registerBuiltInJobs } = await import(
|
|
283
|
+
"../packages/queue/src/index.js"
|
|
284
|
+
);
|
|
285
|
+
await setupQueue(database);
|
|
286
|
+
registerBuiltInJobs();
|
|
287
|
+
// `nexa queue:work` = loop · `nexa queue:work once` or a number = single batch
|
|
288
|
+
if (arg === "once" || (arg && /^\d+$/.test(arg))) {
|
|
289
|
+
const n = await processJobs(database, arg === "once" ? 10 : Number(arg));
|
|
290
|
+
console.log(`Processed ${n} job(s)`);
|
|
291
|
+
} else {
|
|
292
|
+
console.log("Queue worker running (Ctrl+C to stop)…");
|
|
293
|
+
await workQueue(database);
|
|
294
|
+
}
|
|
295
|
+
} else if (command === "schedule:work") {
|
|
296
|
+
await loadApp();
|
|
297
|
+
const { loadEnv } = await import("../packages/core/src/config.js");
|
|
298
|
+
const { startScheduler } = await import("../packages/schedule/src/index.js");
|
|
299
|
+
loadEnv();
|
|
300
|
+
try {
|
|
301
|
+
await import(new URL(`file:///${cwd}/schedule.ts`).href);
|
|
302
|
+
} catch {
|
|
303
|
+
console.log("No schedule.ts");
|
|
304
|
+
}
|
|
305
|
+
startScheduler();
|
|
306
|
+
} else if (command === "make:resource" && arg) {
|
|
307
|
+
const name = arg.toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
308
|
+
mkdirSync("resources", { recursive: true });
|
|
309
|
+
let pkgJson = "";
|
|
310
|
+
try {
|
|
311
|
+
pkgJson = readFileSync("package.json", "utf8");
|
|
312
|
+
} catch {
|
|
313
|
+
/* no package.json */
|
|
314
|
+
}
|
|
315
|
+
const deps = pkgJson ? (JSON.parse(pkgJson).dependencies ?? {}) : {};
|
|
316
|
+
const pkgName = [PKG, "@nexa-stack/framework", "@nexa/framework", "@nexa/core", "nexa-bun", "nexa"].find((n) => n in deps);
|
|
317
|
+
const imp = pkgName ? `"${pkgName}"` : `"../packages/core/src/index.js"`;
|
|
318
|
+
const path = `resources/${name}.ts`;
|
|
319
|
+
writeFileSync(
|
|
320
|
+
path,
|
|
321
|
+
`import { resource, string } from ${imp};
|
|
322
|
+
|
|
323
|
+
resource("${name}", {
|
|
324
|
+
name: string().required().label("Name"),
|
|
325
|
+
}, { label: "${name}" }).admin();
|
|
326
|
+
`
|
|
327
|
+
);
|
|
328
|
+
console.log(`Created ${path} — auto-loaded by nexa serve (no import needed)`);
|
|
329
|
+
} else if (command === "test") {
|
|
330
|
+
const r = spawnSync("npx", ["vitest", "run"], { stdio: "inherit", shell: true });
|
|
331
|
+
process.exit(r.status ?? 1);
|
|
332
|
+
} else {
|
|
333
|
+
console.log(help);
|
|
334
|
+
}
|
package/docs/AI.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# AI Instructions — Nexa Framework
|
|
2
|
+
|
|
3
|
+
> **Read this before generating any code for this project.**
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
This project uses **Nexa** — a Node.js ≥ 20 framework (`@nexa-stack/framework`).
|
|
8
|
+
|
|
9
|
+
NOT Express. NOT NestJS. NOT Laravel.
|
|
10
|
+
|
|
11
|
+
## No controllers, routes, or page files
|
|
12
|
+
|
|
13
|
+
| Do NOT generate | Nexa equivalent |
|
|
14
|
+
|-----------------|-----------------|
|
|
15
|
+
| Controller.php | `resource()` in app.ts |
|
|
16
|
+
| routes/web.php | auto `/api/{name}` |
|
|
17
|
+
| models/ | `resource()` fields |
|
|
18
|
+
| add.blade.php | `.admin()` auto UI |
|
|
19
|
+
| Manual i18n everywhere | `{ label: "Clients" }` or `labelKey` |
|
|
20
|
+
|
|
21
|
+
## Import
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
start, resource, string, number, money, email, text, boolean, belongsTo, getDb,
|
|
26
|
+
} from "@nexa-stack/framework";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Template
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
resource("MODULE", {
|
|
33
|
+
field: string().required(),
|
|
34
|
+
price: money().required(),
|
|
35
|
+
parent: belongsTo("OTHER").required(),
|
|
36
|
+
}, { label: "Label" }).admin().auth();
|
|
37
|
+
|
|
38
|
+
await start();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Example modules
|
|
42
|
+
|
|
43
|
+
| Resource | Label | Auth |
|
|
44
|
+
|----------|-------|------|
|
|
45
|
+
| clients | Clients | no |
|
|
46
|
+
| suppliers | Suppliers | no |
|
|
47
|
+
| products | Products | no |
|
|
48
|
+
| employees | Employees | yes |
|
|
49
|
+
| orders | Orders | yes |
|
|
50
|
+
| invoices | Invoices | yes |
|
|
51
|
+
| payments | Payments | yes |
|
|
52
|
+
|
|
53
|
+
## Checklist
|
|
54
|
+
|
|
55
|
+
- [ ] Edit `app.ts` / `resources/*.ts`
|
|
56
|
+
- [ ] Use `resource()` for each module
|
|
57
|
+
- [ ] English labels by default: `{ label: "..." }`
|
|
58
|
+
- [ ] `.admin()` for UI
|
|
59
|
+
- [ ] `.auth()` for protected modules
|
|
60
|
+
- [ ] No manual routes/controllers/pages
|
|
61
|
+
- [ ] Default locale is English (`APP_LOCALE=en`)
|
|
62
|
+
|
|
63
|
+
## Test
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm run serve
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
http://localhost:3333/admin
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Nexa — Architecture
|
|
2
|
+
|
|
3
|
+
## What Nexa does NOT have
|
|
4
|
+
|
|
5
|
+
Nexa is **not** Laravel / Express. There are no:
|
|
6
|
+
|
|
7
|
+
| Traditional | In Nexa |
|
|
8
|
+
|-------------|---------|
|
|
9
|
+
| Controllers | ❌ — auto-generated from `resource()` |
|
|
10
|
+
| Routes file | ❌ — auto: `/api/{name}` |
|
|
11
|
+
| Models | ❌ — `resource()` defines the table |
|
|
12
|
+
| Migrations | ❌ — tables created on `start()` |
|
|
13
|
+
| Add/Edit pages | ❌ — `.admin()` generates UI |
|
|
14
|
+
| Language files (lang/) | ❌ — labels inline in `resource()` |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What Nexa HAS
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
app.ts → your entire backend (resources + start)
|
|
22
|
+
packages/ → the framework engine
|
|
23
|
+
public/ → custom HTML pages (optional)
|
|
24
|
+
docs/ → documentation
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## How to add a module
|
|
30
|
+
|
|
31
|
+
**One line in `app.ts`:**
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
resource("clients", {
|
|
35
|
+
name: string().required(),
|
|
36
|
+
phone: string().required(),
|
|
37
|
+
}, { label: "Clients" }).admin();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This gives you:
|
|
41
|
+
- Database table `clients`
|
|
42
|
+
- API: GET/POST/PUT/DELETE `/api/clients`
|
|
43
|
+
- Admin page with add form + list
|
|
44
|
+
- Validation
|
|
45
|
+
|
|
46
|
+
**No controller. No route. No add page file.**
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## How to add a custom page
|
|
51
|
+
|
|
52
|
+
Only when `.admin()` is not enough (e.g. cashier POS):
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
public/my-page.html → http://localhost:3333/my-page
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The page uses `fetch()` to call Nexa API.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Flow
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
app.ts
|
|
66
|
+
resource("clients", {...}).admin()
|
|
67
|
+
↓
|
|
68
|
+
start()
|
|
69
|
+
↓
|
|
70
|
+
┌─────────────┬──────────────┬─────────────┐
|
|
71
|
+
│ SQLite DB │ REST API │ Admin UI │
|
|
72
|
+
│ clients │ /api/clients│ /admin │
|
|
73
|
+
└─────────────┴──────────────┴─────────────┘
|
|
74
|
+
```
|