@shohaghinfo/aerojs 0.1.0 → 0.1.2
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/AEROJS.md +12 -12
- package/AGENTS.md +709 -709
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +337 -344
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/new.js +51 -51
- package/dist/cli/templates/fullstack-templates.d.ts +3 -7
- package/dist/cli/templates/fullstack-templates.d.ts.map +1 -1
- package/dist/cli/templates/fullstack-templates.js +540 -13
- package/dist/cli/templates/fullstack-templates.js.map +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,709 +1,709 @@
|
|
|
1
|
-
# AeroJS AI Agent Architecture & Coding Guidelines
|
|
2
|
-
|
|
3
|
-
> **Notice for AI Coding Assistants (Gemini, Claude, Cursor, Windsurf, Copilot, ChatGPT):**
|
|
4
|
-
> This file is your canonical reference manual for writing correct, idiomatic, high-performance TypeScript code for applications built on **AeroJS**. Always adhere strictly to the conventions, patterns, and APIs documented here.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 1. Framework Philosophy & Core Concepts
|
|
9
|
-
|
|
10
|
-
- **Framework Name**: **AeroJS** (imported as `from 'aerojs'`)
|
|
11
|
-
- **Package Identity**: `@aerojs` or `aerojs`
|
|
12
|
-
- **Architecture**: Modern, layered Full-Stack MVC & Service-Oriented Architecture.
|
|
13
|
-
- **Runtime**: Node.js (ESM modules, `"type": "module"`).
|
|
14
|
-
- **Core Standard**: Zero external runtime dependencies in core engine. High-throughput HTTP routing, built-in Active Record ORM, DI container, and flexible view/frontend drivers.
|
|
15
|
-
|
|
16
|
-
### Layered Architecture Flow
|
|
17
|
-
```
|
|
18
|
-
Incoming HTTP Request
|
|
19
|
-
│
|
|
20
|
-
[Middleware] (CORS, Security Headers, CSRF, Rate Limiting, Auth)
|
|
21
|
-
│
|
|
22
|
-
[Router] (Matches Path & HTTP Verb)
|
|
23
|
-
│
|
|
24
|
-
[Validator] (Validates request schema / payload before handler)
|
|
25
|
-
│
|
|
26
|
-
[Controller] (Extracts request inputs, calls Service layer)
|
|
27
|
-
│
|
|
28
|
-
[Service Layer] (Business logic, transactions, external APIs)
|
|
29
|
-
│
|
|
30
|
-
[Active Record Model / QueryBuilder / Database]
|
|
31
|
-
│
|
|
32
|
-
[Response / View Engine / Inertia.js]
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 2. Directory Structure & Responsibilities
|
|
38
|
-
|
|
39
|
-
| Path | Responsibility | Permitted Imports & Logic |
|
|
40
|
-
| :--- | :--- | :--- |
|
|
41
|
-
| `app/controllers/` | HTTP handling, status codes, delegating to services | Models, Services, Validators, `AeroContext` |
|
|
42
|
-
| `app/services/` | Reusable business logic, multi-model transactions | Models, `DB`, External APIs, Jobs, Mail |
|
|
43
|
-
| `app/models/` | Active Record entity definitions, relationships, hooks | `Model`, `Relation` from `aerojs` |
|
|
44
|
-
| `app/validators/` | Request schemas (JSON Schema / VineJS) | Schema validation utilities |
|
|
45
|
-
| `app/middleware/` | Request interception, authentication, logging | `AeroContext`, `NextFunction` from `aerojs` |
|
|
46
|
-
| `app/jobs/` | Asynchronous background tasks | `Job` from `aerojs` |
|
|
47
|
-
| `config/` | Application configuration modules | Reads `process.env` |
|
|
48
|
-
| `database/migrations/` | Database table creation & schema evolution | `Schema`, `TableBlueprint`, `Migration` from `aerojs` |
|
|
49
|
-
| `routes/` | Route definitions (`api.ts`, `web.ts`) | Controllers, Middlewares, Validators |
|
|
50
|
-
| `public/` | Public static assets (CSS, JS, images, robots.txt) | Static files only |
|
|
51
|
-
| `storage/` | Runtime logs, uploaded files, cache | Disk I/O |
|
|
52
|
-
| `tests/` | In-process integration & unit tests | `createTestClient`, `vitest` |
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 3. Routing & Controllers
|
|
57
|
-
|
|
58
|
-
### 3.1 Defining Routes (`routes/api.ts` & `routes/web.ts`)
|
|
59
|
-
|
|
60
|
-
Always group related routes and use **Controller Tuples** `[ControllerClass, 'methodName']`:
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import type { Router } from 'aerojs';
|
|
64
|
-
import { UserController } from '../app/controllers/UserController.js';
|
|
65
|
-
import { authMiddleware } from '../app/middleware/AuthMiddleware.js';
|
|
66
|
-
import { createUserSchema } from '../app/validators/UserValidator.js';
|
|
67
|
-
|
|
68
|
-
export function registerApiRoutes(router: Router): void {
|
|
69
|
-
router.group('/api/v1', (api) => {
|
|
70
|
-
// Public routes
|
|
71
|
-
api.get('/health', async (ctx) => {
|
|
72
|
-
ctx.json({ status: 'ok', uptime: process.uptime() });
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Resource routes
|
|
76
|
-
api.group('/users', (users) => {
|
|
77
|
-
users.get('/', [UserController, 'index']);
|
|
78
|
-
users.get('/:id', [UserController, 'show']);
|
|
79
|
-
users.post('/', [UserController, 'store']).schema(createUserSchema);
|
|
80
|
-
users.put('/:id', [UserController, 'update']).middleware(authMiddleware);
|
|
81
|
-
users.delete('/:id', [UserController, 'destroy']).middleware(authMiddleware);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 3.2 Writing Controllers (`app/controllers/`)
|
|
88
|
-
|
|
89
|
-
Controllers must remain thin. Never write SQL queries or heavy business logic directly inside controllers; call the **Service Layer**:
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
import type { AeroContext } from 'aerojs';
|
|
93
|
-
import { UserService } from '../services/UserService.js';
|
|
94
|
-
|
|
95
|
-
export class UserController {
|
|
96
|
-
private userService = new UserService();
|
|
97
|
-
|
|
98
|
-
public async index(ctx: AeroContext): Promise<void> {
|
|
99
|
-
const page = parseInt((ctx.req.query.page as string) || '1', 10);
|
|
100
|
-
const users = await this.userService.paginate(page);
|
|
101
|
-
ctx.status(200).json({ success: true, data: users });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
public async show(ctx: AeroContext): Promise<void> {
|
|
105
|
-
const id = ctx.req.params.id;
|
|
106
|
-
const user = await this.userService.findById(id);
|
|
107
|
-
if (!user) {
|
|
108
|
-
ctx.status(404).json({ error: 'User not found' });
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
ctx.status(200).json({ success: true, data: user });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
public async store(ctx: AeroContext): Promise<void> {
|
|
115
|
-
const payload = ctx.body as any;
|
|
116
|
-
const newUser = await this.userService.create(payload);
|
|
117
|
-
ctx.status(201).json({ success: true, data: newUser });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### 3.3 AeroContext Cheatsheet
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// Reading Inputs
|
|
126
|
-
const id = ctx.req.params.id; // Route parameters: /users/:id
|
|
127
|
-
const search = ctx.req.query.q; // Query string: ?q=term
|
|
128
|
-
const body = ctx.body; // Parsed JSON or UrlEncoded body
|
|
129
|
-
const auth = ctx.req.get('authorization'); // Request headers
|
|
130
|
-
const cookie = ctx.cookies.get('token'); // Read cookies
|
|
131
|
-
|
|
132
|
-
// Sending Responses
|
|
133
|
-
ctx.status(200).json({ key: 'val' }); // JSON response
|
|
134
|
-
ctx.html('<h1>Hello World</h1>'); // HTML response
|
|
135
|
-
ctx.text('Plain text message'); // Plain text
|
|
136
|
-
ctx.redirect('/dashboard', 302); // Redirect
|
|
137
|
-
ctx.cookies.set('token', 'xyz', { // Set cookie
|
|
138
|
-
httpOnly: true,
|
|
139
|
-
secure: true,
|
|
140
|
-
maxAge: 3600,
|
|
141
|
-
});
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## 4. Database, Active Record ORM & Migrations
|
|
147
|
-
|
|
148
|
-
AeroJS features a first-class, built-in ORM with Active Record models, fluent QueryBuilder, and schema migrations.
|
|
149
|
-
|
|
150
|
-
### 4.1 Active Record Models (`app/models/`)
|
|
151
|
-
|
|
152
|
-
Define models by extending `Model`:
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
import { Model } from 'aerojs';
|
|
156
|
-
import { Post } from './Post.js';
|
|
157
|
-
import { Profile } from './Profile.js';
|
|
158
|
-
|
|
159
|
-
export class User extends Model {
|
|
160
|
-
public static override table = 'users';
|
|
161
|
-
public static override primaryKey = 'id';
|
|
162
|
-
public static override fillable = ['name', 'email', 'role', 'password'];
|
|
163
|
-
public static override hidden = ['password'];
|
|
164
|
-
public static override softDeletes = true; // Enables deleted_at handling
|
|
165
|
-
|
|
166
|
-
// Relationships
|
|
167
|
-
public profile() {
|
|
168
|
-
return this.hasOne(Profile, 'user_id');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
public posts() {
|
|
172
|
-
return this.hasMany(Post, 'user_id');
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
#### Model Query & Persistence Operations
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
// Finding Records
|
|
181
|
-
const user = await User.find(1);
|
|
182
|
-
const user = await User.findOrFail(1); // Throws NotFoundError if missing
|
|
183
|
-
const admin = await User.where('role', 'admin').first();
|
|
184
|
-
const allActive = await User.where('is_active', true).get();
|
|
185
|
-
|
|
186
|
-
// Eager Loading Relationships
|
|
187
|
-
const usersWithPosts = await User.with('profile', 'posts').get();
|
|
188
|
-
|
|
189
|
-
// Creating Records
|
|
190
|
-
const newUser = await User.create({
|
|
191
|
-
name: 'Jane Doe',
|
|
192
|
-
email: 'jane@example.com',
|
|
193
|
-
role: 'developer',
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Updating Records
|
|
197
|
-
user.name = 'Jane Smith';
|
|
198
|
-
await user.save();
|
|
199
|
-
|
|
200
|
-
// Deleting Records
|
|
201
|
-
await user.delete(); // Soft deletes if softDeletes = true
|
|
202
|
-
await user.restore(); // Restores soft-deleted record
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### 4.2 Fluent QueryBuilder (`DB.table(...)`)
|
|
206
|
-
|
|
207
|
-
When complex aggregate queries, custom joins, or bulk operations are needed:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
import { DB } from 'aerojs';
|
|
211
|
-
|
|
212
|
-
// Select with conditions, joins & pagination
|
|
213
|
-
const results = await DB.table('orders')
|
|
214
|
-
.select('orders.*', 'users.name as customer_name')
|
|
215
|
-
.join('users', 'orders.user_id', 'users.id')
|
|
216
|
-
.where('orders.status', '=', 'completed')
|
|
217
|
-
.whereIn('orders.currency', ['USD', 'EUR'])
|
|
218
|
-
.orderBy('orders.created_at', 'DESC')
|
|
219
|
-
.paginate(1, 15); // Returns { data, total, page, perPage, lastPage }
|
|
220
|
-
|
|
221
|
-
// Aggregates
|
|
222
|
-
const totalRevenue = await DB.table('orders').sum('total_amount');
|
|
223
|
-
const orderCount = await DB.table('orders').where('status', 'pending').count();
|
|
224
|
-
|
|
225
|
-
// Direct Insert, Update, Delete
|
|
226
|
-
await DB.table('logs').insert({ event: 'login', ip: '127.0.0.1' });
|
|
227
|
-
await DB.table('users').where('id', 5).update({ status: 'active' });
|
|
228
|
-
await DB.table('sessions').where('expires_at', '<', new Date()).delete();
|
|
229
|
-
|
|
230
|
-
// Raw SQL & Transactions
|
|
231
|
-
const rawRows = await DB.query('SELECT * FROM users WHERE email = ?', ['admin@dev.com']);
|
|
232
|
-
|
|
233
|
-
await DB.transaction(async (trx) => {
|
|
234
|
-
await trx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
|
|
235
|
-
await trx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
|
|
236
|
-
});
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
### 4.3 Database Migrations (`database/migrations/`)
|
|
240
|
-
|
|
241
|
-
Migrations define schema changes using `TableBlueprint`:
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
import { Schema, type Migration, type TableBlueprint } from 'aerojs';
|
|
245
|
-
|
|
246
|
-
export default class CreateUsersTable implements Migration {
|
|
247
|
-
public name = '2026_09_30_000000_create_users_table';
|
|
248
|
-
|
|
249
|
-
public async up(): Promise<void> {
|
|
250
|
-
await Schema.create('users', (table: TableBlueprint) => {
|
|
251
|
-
table.increments('id');
|
|
252
|
-
table.string('name', 255).notNull();
|
|
253
|
-
table.string('email', 191).notNull().unique();
|
|
254
|
-
table.string('role').defaultTo('user');
|
|
255
|
-
table.boolean('is_active').defaultTo(true);
|
|
256
|
-
table.text('bio').nullable();
|
|
257
|
-
table.timestamps(); // Creates created_at and updated_at
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
public async down(): Promise<void> {
|
|
262
|
-
await Schema.dropIfExists('users');
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
#### Blueprint Column Methods
|
|
268
|
-
- `table.increments('id')`
|
|
269
|
-
- `table.string('name', 255)`
|
|
270
|
-
- `table.integer('age')`
|
|
271
|
-
- `table.boolean('is_verified')`
|
|
272
|
-
- `table.text('content')`
|
|
273
|
-
- `table.timestamp('published_at')`
|
|
274
|
-
- `table.timestamps()`
|
|
275
|
-
- `.nullable()`, `.notNull()`, `.unique()`, `.defaultTo(val)`
|
|
276
|
-
|
|
277
|
-
### 4.4 Database Adapters, Dialects & Connection Pooling
|
|
278
|
-
|
|
279
|
-
AeroJS supports multiple relational databases via Knex connection pooling, as well as native zero-dependency in-memory execution.
|
|
280
|
-
|
|
281
|
-
#### Environment Variables (`.env`):
|
|
282
|
-
```env
|
|
283
|
-
# Database (MySQL / PostgreSQL / SQLite)
|
|
284
|
-
DB_CONNECTION=mysql
|
|
285
|
-
DB_HOST=127.0.0.1
|
|
286
|
-
DB_PORT=3306
|
|
287
|
-
DB_USER=root
|
|
288
|
-
DB_PASSWORD=
|
|
289
|
-
DB_DATABASE=aerojs_app
|
|
290
|
-
DB_POOL_MIN=2
|
|
291
|
-
DB_POOL_MAX=20
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
#### How Dialects & Pooling Work in `config/database.ts`:
|
|
295
|
-
```typescript
|
|
296
|
-
const poolMin = parseInt(process.env.DB_POOL_MIN || '2', 10);
|
|
297
|
-
const poolMax = parseInt(process.env.DB_POOL_MAX || '20', 10);
|
|
298
|
-
|
|
299
|
-
export const databaseConfig = {
|
|
300
|
-
default: (process.env.DB_CONNECTION || 'mysql').toLowerCase(),
|
|
301
|
-
pool: { min: poolMin, max: poolMax },
|
|
302
|
-
connections: {
|
|
303
|
-
mysql: { client: 'mysql2', ... },
|
|
304
|
-
postgres: { client: 'pg', ... },
|
|
305
|
-
sqlite: { client: 'better-sqlite3', ... },
|
|
306
|
-
memory: { client: 'memory' },
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
#### 1. Using MySQL (`client: 'mysql2'`)
|
|
312
|
-
```typescript
|
|
313
|
-
import knex from 'knex';
|
|
314
|
-
import { useKnex } from 'aerojs';
|
|
315
|
-
import { databaseConfig } from './config/database.js';
|
|
316
|
-
|
|
317
|
-
const db = knex({
|
|
318
|
-
client: 'mysql2',
|
|
319
|
-
connection: databaseConfig.connections.mysql,
|
|
320
|
-
pool: databaseConfig.pool,
|
|
321
|
-
});
|
|
322
|
-
useKnex(db); // Seamlessly binds to all Models and DB.table()
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
#### 2. Using PostgreSQL (`client: 'pg'`)
|
|
326
|
-
Change `.env` to `DB_CONNECTION=postgres`, `DB_PORT=5432`, `DB_USER=postgres`:
|
|
327
|
-
```typescript
|
|
328
|
-
import knex from 'knex';
|
|
329
|
-
import { useKnex } from 'aerojs';
|
|
330
|
-
import { databaseConfig } from './config/database.js';
|
|
331
|
-
|
|
332
|
-
const db = knex({
|
|
333
|
-
client: 'pg',
|
|
334
|
-
connection: databaseConfig.connections.postgres,
|
|
335
|
-
pool: databaseConfig.pool,
|
|
336
|
-
});
|
|
337
|
-
useKnex(db);
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
#### 3. Using SQLite (`client: 'better-sqlite3'`)
|
|
341
|
-
Change `.env` to `DB_CONNECTION=sqlite`, `DB_DATABASE=storage/database.sqlite`:
|
|
342
|
-
```typescript
|
|
343
|
-
import knex from 'knex';
|
|
344
|
-
import { useKnex } from 'aerojs';
|
|
345
|
-
import { databaseConfig } from './config/database.js';
|
|
346
|
-
|
|
347
|
-
const db = knex({
|
|
348
|
-
client: 'better-sqlite3',
|
|
349
|
-
connection: databaseConfig.connections.sqlite,
|
|
350
|
-
useNullAsDefault: true,
|
|
351
|
-
});
|
|
352
|
-
useKnex(db);
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
#### 4. Connecting Prisma or Drizzle
|
|
356
|
-
```typescript
|
|
357
|
-
import { usePrisma, useDrizzle } from 'aerojs';
|
|
358
|
-
|
|
359
|
-
// Prisma
|
|
360
|
-
usePrisma(prismaClient);
|
|
361
|
-
|
|
362
|
-
// Drizzle
|
|
363
|
-
useDrizzle(drizzleDb);
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## 5. Frontend & View Engines (Vue, React, Edge.js, EJS)
|
|
369
|
-
|
|
370
|
-
AeroJS supports multiple presentation tiers:
|
|
371
|
-
|
|
372
|
-
### 5.1 Inertia.js (React & Vue 3)
|
|
373
|
-
|
|
374
|
-
For modern Single Page Applications with server-side routing:
|
|
375
|
-
|
|
376
|
-
1. **Enable in `server.ts`**:
|
|
377
|
-
```typescript
|
|
378
|
-
app.useInertia({
|
|
379
|
-
rootView: 'default', // Or custom HTML root template with vite() tags
|
|
380
|
-
version: '1.0',
|
|
381
|
-
});
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
2. **Render in Controller**:
|
|
385
|
-
```typescript
|
|
386
|
-
export class UserController {
|
|
387
|
-
public async index(ctx: AeroContext): Promise<void> {
|
|
388
|
-
const users = await User.all();
|
|
389
|
-
await ctx.inertia.render('Users/Index', {
|
|
390
|
-
users,
|
|
391
|
-
title: 'User Management',
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
### 5.2 Official Vite Integration (`vite.config.ts` for React & Vue 3)
|
|
398
|
-
|
|
399
|
-
AeroJS integrates seamlessly with **Vite** for ultra-fast Hot Module Replacement (HMR) during development and optimized asset hashing in production.
|
|
400
|
-
|
|
401
|
-
#### 1. Directory Structure:
|
|
402
|
-
```text
|
|
403
|
-
my-aero-app/
|
|
404
|
-
├── resources/
|
|
405
|
-
│ ├── js/
|
|
406
|
-
│ │ └── app.ts # Vue 3 / React entry point
|
|
407
|
-
│ └── css/
|
|
408
|
-
│ └── app.css # Tailwind CSS / Styles
|
|
409
|
-
├── public/
|
|
410
|
-
│ └── build/ # Generated production assets & manifest.json
|
|
411
|
-
└── vite.config.ts # Vite configuration
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
#### 2. `vite.config.ts` Configuration:
|
|
415
|
-
```typescript
|
|
416
|
-
import { defineConfig } from 'vite';
|
|
417
|
-
// For React: import react from '@vitejs/plugin-react';
|
|
418
|
-
// For Vue 3: import vue from '@vitejs/plugin-vue';
|
|
419
|
-
|
|
420
|
-
export default defineConfig({
|
|
421
|
-
plugins: [
|
|
422
|
-
// react(), // or vue()
|
|
423
|
-
],
|
|
424
|
-
build: {
|
|
425
|
-
outDir: 'public/build',
|
|
426
|
-
manifest: true, // Generates manifest.json with hashed chunks
|
|
427
|
-
rollupOptions: {
|
|
428
|
-
input: 'resources/js/app.ts', // or resources/js/app.tsx
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
server: {
|
|
432
|
-
cors: true,
|
|
433
|
-
port: 5173,
|
|
434
|
-
strictPort: true,
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
#### 3. Injecting Vite Assets with AeroJS `vite()` Helper:
|
|
440
|
-
AeroJS provides a built-in `vite(entry)` helper that auto-detects development vs production:
|
|
441
|
-
- In Development: Injects `<script type="module" src="http://localhost:5173/@vite/client">` + entry scripts.
|
|
442
|
-
- In Production: Automatically reads `public/build/manifest.json` and injects hashed `<link rel="stylesheet">` and `<script>` tags.
|
|
443
|
-
|
|
444
|
-
```typescript
|
|
445
|
-
import { vite } from 'aerojs';
|
|
446
|
-
|
|
447
|
-
// In your root Inertia view or HTML layout:
|
|
448
|
-
app.useInertia({
|
|
449
|
-
rootView: `
|
|
450
|
-
<!DOCTYPE html>
|
|
451
|
-
<html lang="en">
|
|
452
|
-
<head>
|
|
453
|
-
<meta charset="UTF-8">
|
|
454
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
455
|
-
${vite('resources/js/app.ts')}
|
|
456
|
-
</head>
|
|
457
|
-
<body>
|
|
458
|
-
@inertia
|
|
459
|
-
</body>
|
|
460
|
-
</html>
|
|
461
|
-
`,
|
|
462
|
-
});
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
#### 4. NPM Scripts (`package.json`):
|
|
466
|
-
```json
|
|
467
|
-
{
|
|
468
|
-
"scripts": {
|
|
469
|
-
"dev": "concurrently \"tsx watch server.ts\" \"vite\"",
|
|
470
|
-
"build": "tsc && vite build",
|
|
471
|
-
"start": "node dist/server.js"
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
### 5.3 Edge.js Template Engine (AdonisJS Style)
|
|
477
|
-
|
|
478
|
-
```typescript
|
|
479
|
-
import { Edge } from 'edge.js';
|
|
480
|
-
import { createEdgeDriver } from 'aerojs';
|
|
481
|
-
|
|
482
|
-
const edge = new Edge();
|
|
483
|
-
edge.mount(new URL('./views', import.meta.url));
|
|
484
|
-
app.useViewEngine(createEdgeDriver(edge));
|
|
485
|
-
|
|
486
|
-
// In Route / Controller:
|
|
487
|
-
router.get('/dashboard', async (ctx) => {
|
|
488
|
-
await ctx.view('dashboard', { user: ctx.state.user });
|
|
489
|
-
});
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### 5.3 EJS Template Engine
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
import ejs from 'ejs';
|
|
496
|
-
import { createEjsDriver } from 'aerojs';
|
|
497
|
-
|
|
498
|
-
app.useViewEngine(createEjsDriver(ejs));
|
|
499
|
-
|
|
500
|
-
// In Route / Controller:
|
|
501
|
-
router.get('/about', async (ctx) => {
|
|
502
|
-
await ctx.view('pages/about.ejs', { title: 'About Us' });
|
|
503
|
-
});
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### 5.4 SSR Engine
|
|
507
|
-
|
|
508
|
-
AeroJS includes a built-in `SSREngine` supporting asynchronous streaming and string rendering for server-rendered React or Vue components.
|
|
509
|
-
|
|
510
|
-
---
|
|
511
|
-
|
|
512
|
-
## 6. Request Validation (`app/validators/`)
|
|
513
|
-
|
|
514
|
-
Always validate request payloads before they reach business logic:
|
|
515
|
-
|
|
516
|
-
```typescript
|
|
517
|
-
export const createUserSchema = {
|
|
518
|
-
body: {
|
|
519
|
-
type: 'object',
|
|
520
|
-
required: ['name', 'email'],
|
|
521
|
-
properties: {
|
|
522
|
-
name: { type: 'string', minLength: 2, maxLength: 100 },
|
|
523
|
-
email: { type: 'string', format: 'email' },
|
|
524
|
-
role: { type: 'string', enum: ['admin', 'developer', 'user'] },
|
|
525
|
-
},
|
|
526
|
-
},
|
|
527
|
-
};
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
Attach to routes using `.schema(createUserSchema)`. AeroJS will automatically reject invalid requests with status `422 Unprocessable Entity`.
|
|
531
|
-
|
|
532
|
-
---
|
|
533
|
-
|
|
534
|
-
## 7. Background Queue Jobs & Mail
|
|
535
|
-
|
|
536
|
-
### 7.1 Jobs (`app/jobs/`)
|
|
537
|
-
|
|
538
|
-
```typescript
|
|
539
|
-
import { Job, Queue } from 'aerojs';
|
|
540
|
-
|
|
541
|
-
export class SendWelcomeEmailJob extends Job {
|
|
542
|
-
public static override queue = 'emails';
|
|
543
|
-
public static override maxTries = 3;
|
|
544
|
-
|
|
545
|
-
public async handle(): Promise<void> {
|
|
546
|
-
const { email, name } = this.data;
|
|
547
|
-
console.log(`Sending welcome email to ${name} (${email})`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Dispatching a Job:
|
|
552
|
-
await Queue.push(new SendWelcomeEmailJob({ email: 'user@aerojs.dev', name: 'User' }));
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### 7.2 Mail Sending
|
|
556
|
-
|
|
557
|
-
```typescript
|
|
558
|
-
import { Mail } from 'aerojs';
|
|
559
|
-
|
|
560
|
-
await Mail.send({
|
|
561
|
-
to: 'user@example.com',
|
|
562
|
-
subject: 'Welcome to our platform',
|
|
563
|
-
html: '<h1>Welcome!</h1><p>Your account is now ready.</p>',
|
|
564
|
-
});
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
## 8. Testing Conventions (`tests/`)
|
|
570
|
-
|
|
571
|
-
Use AeroJS `createTestClient` with Vitest for fast, in-memory end-to-end testing without opening real network sockets:
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
|
-
import { describe, it, expect } from 'vitest';
|
|
575
|
-
import { createTestClient } from 'aerojs/testing';
|
|
576
|
-
import app from '../server.js';
|
|
577
|
-
|
|
578
|
-
describe('User API Tests', () => {
|
|
579
|
-
const client = createTestClient(app);
|
|
580
|
-
|
|
581
|
-
it('GET /api/users returns list of users', async () => {
|
|
582
|
-
const res = await client.get('/api/users');
|
|
583
|
-
expect(res.status).toBe(200);
|
|
584
|
-
expect(res.json().success).toBe(true);
|
|
585
|
-
expect(Array.isArray(res.json().data)).toBe(true);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
it('POST /api/users validates payload', async () => {
|
|
589
|
-
const res = await client.post('/api/users', {
|
|
590
|
-
body: { name: 'A' }, // Missing email & name too short
|
|
591
|
-
});
|
|
592
|
-
expect(res.status).toBe(422);
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
### 8.2 Next.js Style Interactive Error Dashboard
|
|
598
|
-
|
|
599
|
-
In development mode (`debug: true` or `NODE_ENV !== 'production'`):
|
|
600
|
-
- **Browser Requests (`Accept: text/html`)**: AeroJS intercepts unhandled exceptions and renders a rich dark-mode Error Dashboard. It parses the stack trace, extracts the local source code file, and highlights the exact crashing line with an error indicator (`→`). It also provides interactive call stack inspection, request headers/parameters explorer, and system diagnostics.
|
|
601
|
-
- **API Requests (`Accept: application/json`)**: AeroJS returns clean JSON with `{ error: { message, status, code, stack } }`.
|
|
602
|
-
|
|
603
|
-
---
|
|
604
|
-
|
|
605
|
-
## 9. Frontend & View Engines (Edge.js, EJS, React, Vue 3)
|
|
606
|
-
|
|
607
|
-
AeroJS supports both **Server-Side Template Engines (MPA)** and **Modern Single-Page Applications (SPA)** via Inertia.js protocol.
|
|
608
|
-
|
|
609
|
-
### 9.1 Edge.js (AdonisJS Official Template Engine)
|
|
610
|
-
- **Install**: `npm install edge.js`
|
|
611
|
-
- **Location**: `views/edge/*.edge`
|
|
612
|
-
- **Syntax**:
|
|
613
|
-
```edge
|
|
614
|
-
@each(product in products)
|
|
615
|
-
<div class="product-card">
|
|
616
|
-
<h3>{{ product.name }}</h3>
|
|
617
|
-
<span>${{ product.price }}</span>
|
|
618
|
-
@if(product.stock > 10)
|
|
619
|
-
<span class="stock-ok">In Stock ({{ product.stock }})</span>
|
|
620
|
-
@else
|
|
621
|
-
<span class="stock-low">Low Stock ({{ product.stock }})</span>
|
|
622
|
-
@endif
|
|
623
|
-
</div>
|
|
624
|
-
@endeach
|
|
625
|
-
```
|
|
626
|
-
- **Handler**:
|
|
627
|
-
```ts
|
|
628
|
-
import { renderEdge } from '../app/views/engine.js';
|
|
629
|
-
router.get('/views/edge', async (ctx: AeroContext) => {
|
|
630
|
-
const products = await DB.table('products').get();
|
|
631
|
-
const html = await renderEdge('edge/products', { products });
|
|
632
|
-
ctx.html(html);
|
|
633
|
-
});
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
### 9.2 EJS (Embedded JavaScript)
|
|
637
|
-
- **Install**: `npm install ejs && npm install -D @types/ejs`
|
|
638
|
-
- **Location**: `views/ejs/*.ejs`
|
|
639
|
-
- **Syntax**:
|
|
640
|
-
```ejs
|
|
641
|
-
<% products.forEach(function(product) { %>
|
|
642
|
-
<div class="product-card">
|
|
643
|
-
<h3><%= product.name %></h3>
|
|
644
|
-
<span>$<%= product.price %></span>
|
|
645
|
-
<% if (product.stock > 10) { %>
|
|
646
|
-
<span class="stock-ok">In Stock</span>
|
|
647
|
-
<% } else { %>
|
|
648
|
-
<span class="stock-low">Low Stock</span>
|
|
649
|
-
<% } %>
|
|
650
|
-
</div>
|
|
651
|
-
<% }); %>
|
|
652
|
-
```
|
|
653
|
-
- **Handler**:
|
|
654
|
-
```ts
|
|
655
|
-
import { renderEjs } from '../app/views/engine.js';
|
|
656
|
-
router.get('/views/ejs', async (ctx: AeroContext) => {
|
|
657
|
-
const products = await DB.table('products').get();
|
|
658
|
-
const html = await renderEjs('ejs/products.ejs', { products });
|
|
659
|
-
ctx.html(html);
|
|
660
|
-
});
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
### 9.3 React 18 (Inertia.js SPA)
|
|
664
|
-
- **Install**: `@inertiajs/react react react-dom`
|
|
665
|
-
- **Location**: `resources/js/Pages/ProductsReact.tsx`
|
|
666
|
-
- **Handler**:
|
|
667
|
-
```ts
|
|
668
|
-
router.get('/views/react', async (ctx: AeroContext) => {
|
|
669
|
-
const products = await DB.table('products').get();
|
|
670
|
-
await ctx.inertia.render('ProductsReact', { products });
|
|
671
|
-
});
|
|
672
|
-
```
|
|
673
|
-
- **Protocol**:
|
|
674
|
-
- Initial visit from browser: Returns HTML shell with `<div id="app" data-page='{"component":"ProductsReact","props":{...}}'></div>`.
|
|
675
|
-
- AJAX visit with `X-Inertia: true`: Returns lightweight JSON props.
|
|
676
|
-
|
|
677
|
-
### 9.4 Vue 3 (Inertia.js SPA)
|
|
678
|
-
- **Install**: `@inertiajs/vue3 vue`
|
|
679
|
-
- **Location**: `resources/js/Pages/ProductsVue.vue`
|
|
680
|
-
- **Handler**:
|
|
681
|
-
```ts
|
|
682
|
-
router.get('/views/vue', async (ctx: AeroContext) => {
|
|
683
|
-
const products = await DB.table('products').get();
|
|
684
|
-
await ctx.inertia.render('ProductsVue', { products });
|
|
685
|
-
});
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
---
|
|
689
|
-
|
|
690
|
-
## 10. AI Agent Implementation Checklist
|
|
691
|
-
|
|
692
|
-
When asked to build or modify any feature in an AeroJS project:
|
|
693
|
-
|
|
694
|
-
1. **New Route**: Register in `routes/api.ts` or `routes/web.ts` using `router.group()` and controller tuples `[Controller, 'action']`.
|
|
695
|
-
2. **New Controller**: Place in `app/controllers/`. Keep it lean; forward data to `app/services/`.
|
|
696
|
-
3. **New Business Logic**: Place in `app/services/`.
|
|
697
|
-
4. **New Entity / Table**:
|
|
698
|
-
- Create migration in `database/migrations/` using `Schema.create('table_name', (table) => ...)`.
|
|
699
|
-
- Create model in `app/models/` extending `Model`.
|
|
700
|
-
5. **New Input Validation**: Create schema in `app/validators/` and attach via `.schema(...)`.
|
|
701
|
-
6. **Async Tasks / Emails**: Create job in `app/jobs/` extending `Job` and dispatch via `Queue.push()`.
|
|
702
|
-
7. **Frontend Views**:
|
|
703
|
-
- Use Edge.js (`views/edge/`) or EJS (`views/ejs/`) for Server-Rendered HTML.
|
|
704
|
-
- Use Inertia.js React (`resources/js/Pages/*.tsx`) or Vue 3 (`resources/js/Pages/*.vue`) for SPAs.
|
|
705
|
-
8. **Verification**: Always verify changes by running:
|
|
706
|
-
```bash
|
|
707
|
-
npm run build
|
|
708
|
-
npm test
|
|
709
|
-
```
|
|
1
|
+
# AeroJS AI Agent Architecture & Coding Guidelines
|
|
2
|
+
|
|
3
|
+
> **Notice for AI Coding Assistants (Gemini, Claude, Cursor, Windsurf, Copilot, ChatGPT):**
|
|
4
|
+
> This file is your canonical reference manual for writing correct, idiomatic, high-performance TypeScript code for applications built on **AeroJS**. Always adhere strictly to the conventions, patterns, and APIs documented here.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Framework Philosophy & Core Concepts
|
|
9
|
+
|
|
10
|
+
- **Framework Name**: **AeroJS** (imported as `from 'aerojs'`)
|
|
11
|
+
- **Package Identity**: `@aerojs` or `aerojs`
|
|
12
|
+
- **Architecture**: Modern, layered Full-Stack MVC & Service-Oriented Architecture.
|
|
13
|
+
- **Runtime**: Node.js (ESM modules, `"type": "module"`).
|
|
14
|
+
- **Core Standard**: Zero external runtime dependencies in core engine. High-throughput HTTP routing, built-in Active Record ORM, DI container, and flexible view/frontend drivers.
|
|
15
|
+
|
|
16
|
+
### Layered Architecture Flow
|
|
17
|
+
```
|
|
18
|
+
Incoming HTTP Request
|
|
19
|
+
│
|
|
20
|
+
[Middleware] (CORS, Security Headers, CSRF, Rate Limiting, Auth)
|
|
21
|
+
│
|
|
22
|
+
[Router] (Matches Path & HTTP Verb)
|
|
23
|
+
│
|
|
24
|
+
[Validator] (Validates request schema / payload before handler)
|
|
25
|
+
│
|
|
26
|
+
[Controller] (Extracts request inputs, calls Service layer)
|
|
27
|
+
│
|
|
28
|
+
[Service Layer] (Business logic, transactions, external APIs)
|
|
29
|
+
│
|
|
30
|
+
[Active Record Model / QueryBuilder / Database]
|
|
31
|
+
│
|
|
32
|
+
[Response / View Engine / Inertia.js]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Directory Structure & Responsibilities
|
|
38
|
+
|
|
39
|
+
| Path | Responsibility | Permitted Imports & Logic |
|
|
40
|
+
| :--- | :--- | :--- |
|
|
41
|
+
| `app/controllers/` | HTTP handling, status codes, delegating to services | Models, Services, Validators, `AeroContext` |
|
|
42
|
+
| `app/services/` | Reusable business logic, multi-model transactions | Models, `DB`, External APIs, Jobs, Mail |
|
|
43
|
+
| `app/models/` | Active Record entity definitions, relationships, hooks | `Model`, `Relation` from `aerojs` |
|
|
44
|
+
| `app/validators/` | Request schemas (JSON Schema / VineJS) | Schema validation utilities |
|
|
45
|
+
| `app/middleware/` | Request interception, authentication, logging | `AeroContext`, `NextFunction` from `aerojs` |
|
|
46
|
+
| `app/jobs/` | Asynchronous background tasks | `Job` from `aerojs` |
|
|
47
|
+
| `config/` | Application configuration modules | Reads `process.env` |
|
|
48
|
+
| `database/migrations/` | Database table creation & schema evolution | `Schema`, `TableBlueprint`, `Migration` from `aerojs` |
|
|
49
|
+
| `routes/` | Route definitions (`api.ts`, `web.ts`) | Controllers, Middlewares, Validators |
|
|
50
|
+
| `public/` | Public static assets (CSS, JS, images, robots.txt) | Static files only |
|
|
51
|
+
| `storage/` | Runtime logs, uploaded files, cache | Disk I/O |
|
|
52
|
+
| `tests/` | In-process integration & unit tests | `createTestClient`, `vitest` |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. Routing & Controllers
|
|
57
|
+
|
|
58
|
+
### 3.1 Defining Routes (`routes/api.ts` & `routes/web.ts`)
|
|
59
|
+
|
|
60
|
+
Always group related routes and use **Controller Tuples** `[ControllerClass, 'methodName']`:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import type { Router } from 'aerojs';
|
|
64
|
+
import { UserController } from '../app/controllers/UserController.js';
|
|
65
|
+
import { authMiddleware } from '../app/middleware/AuthMiddleware.js';
|
|
66
|
+
import { createUserSchema } from '../app/validators/UserValidator.js';
|
|
67
|
+
|
|
68
|
+
export function registerApiRoutes(router: Router): void {
|
|
69
|
+
router.group('/api/v1', (api) => {
|
|
70
|
+
// Public routes
|
|
71
|
+
api.get('/health', async (ctx) => {
|
|
72
|
+
ctx.json({ status: 'ok', uptime: process.uptime() });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Resource routes
|
|
76
|
+
api.group('/users', (users) => {
|
|
77
|
+
users.get('/', [UserController, 'index']);
|
|
78
|
+
users.get('/:id', [UserController, 'show']);
|
|
79
|
+
users.post('/', [UserController, 'store']).schema(createUserSchema);
|
|
80
|
+
users.put('/:id', [UserController, 'update']).middleware(authMiddleware);
|
|
81
|
+
users.delete('/:id', [UserController, 'destroy']).middleware(authMiddleware);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3.2 Writing Controllers (`app/controllers/`)
|
|
88
|
+
|
|
89
|
+
Controllers must remain thin. Never write SQL queries or heavy business logic directly inside controllers; call the **Service Layer**:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import type { AeroContext } from 'aerojs';
|
|
93
|
+
import { UserService } from '../services/UserService.js';
|
|
94
|
+
|
|
95
|
+
export class UserController {
|
|
96
|
+
private userService = new UserService();
|
|
97
|
+
|
|
98
|
+
public async index(ctx: AeroContext): Promise<void> {
|
|
99
|
+
const page = parseInt((ctx.req.query.page as string) || '1', 10);
|
|
100
|
+
const users = await this.userService.paginate(page);
|
|
101
|
+
ctx.status(200).json({ success: true, data: users });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public async show(ctx: AeroContext): Promise<void> {
|
|
105
|
+
const id = ctx.req.params.id;
|
|
106
|
+
const user = await this.userService.findById(id);
|
|
107
|
+
if (!user) {
|
|
108
|
+
ctx.status(404).json({ error: 'User not found' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
ctx.status(200).json({ success: true, data: user });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public async store(ctx: AeroContext): Promise<void> {
|
|
115
|
+
const payload = ctx.body as any;
|
|
116
|
+
const newUser = await this.userService.create(payload);
|
|
117
|
+
ctx.status(201).json({ success: true, data: newUser });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3.3 AeroContext Cheatsheet
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Reading Inputs
|
|
126
|
+
const id = ctx.req.params.id; // Route parameters: /users/:id
|
|
127
|
+
const search = ctx.req.query.q; // Query string: ?q=term
|
|
128
|
+
const body = ctx.body; // Parsed JSON or UrlEncoded body
|
|
129
|
+
const auth = ctx.req.get('authorization'); // Request headers
|
|
130
|
+
const cookie = ctx.cookies.get('token'); // Read cookies
|
|
131
|
+
|
|
132
|
+
// Sending Responses
|
|
133
|
+
ctx.status(200).json({ key: 'val' }); // JSON response
|
|
134
|
+
ctx.html('<h1>Hello World</h1>'); // HTML response
|
|
135
|
+
ctx.text('Plain text message'); // Plain text
|
|
136
|
+
ctx.redirect('/dashboard', 302); // Redirect
|
|
137
|
+
ctx.cookies.set('token', 'xyz', { // Set cookie
|
|
138
|
+
httpOnly: true,
|
|
139
|
+
secure: true,
|
|
140
|
+
maxAge: 3600,
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 4. Database, Active Record ORM & Migrations
|
|
147
|
+
|
|
148
|
+
AeroJS features a first-class, built-in ORM with Active Record models, fluent QueryBuilder, and schema migrations.
|
|
149
|
+
|
|
150
|
+
### 4.1 Active Record Models (`app/models/`)
|
|
151
|
+
|
|
152
|
+
Define models by extending `Model`:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { Model } from 'aerojs';
|
|
156
|
+
import { Post } from './Post.js';
|
|
157
|
+
import { Profile } from './Profile.js';
|
|
158
|
+
|
|
159
|
+
export class User extends Model {
|
|
160
|
+
public static override table = 'users';
|
|
161
|
+
public static override primaryKey = 'id';
|
|
162
|
+
public static override fillable = ['name', 'email', 'role', 'password'];
|
|
163
|
+
public static override hidden = ['password'];
|
|
164
|
+
public static override softDeletes = true; // Enables deleted_at handling
|
|
165
|
+
|
|
166
|
+
// Relationships
|
|
167
|
+
public profile() {
|
|
168
|
+
return this.hasOne(Profile, 'user_id');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public posts() {
|
|
172
|
+
return this.hasMany(Post, 'user_id');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Model Query & Persistence Operations
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Finding Records
|
|
181
|
+
const user = await User.find(1);
|
|
182
|
+
const user = await User.findOrFail(1); // Throws NotFoundError if missing
|
|
183
|
+
const admin = await User.where('role', 'admin').first();
|
|
184
|
+
const allActive = await User.where('is_active', true).get();
|
|
185
|
+
|
|
186
|
+
// Eager Loading Relationships
|
|
187
|
+
const usersWithPosts = await User.with('profile', 'posts').get();
|
|
188
|
+
|
|
189
|
+
// Creating Records
|
|
190
|
+
const newUser = await User.create({
|
|
191
|
+
name: 'Jane Doe',
|
|
192
|
+
email: 'jane@example.com',
|
|
193
|
+
role: 'developer',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Updating Records
|
|
197
|
+
user.name = 'Jane Smith';
|
|
198
|
+
await user.save();
|
|
199
|
+
|
|
200
|
+
// Deleting Records
|
|
201
|
+
await user.delete(); // Soft deletes if softDeletes = true
|
|
202
|
+
await user.restore(); // Restores soft-deleted record
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 4.2 Fluent QueryBuilder (`DB.table(...)`)
|
|
206
|
+
|
|
207
|
+
When complex aggregate queries, custom joins, or bulk operations are needed:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { DB } from 'aerojs';
|
|
211
|
+
|
|
212
|
+
// Select with conditions, joins & pagination
|
|
213
|
+
const results = await DB.table('orders')
|
|
214
|
+
.select('orders.*', 'users.name as customer_name')
|
|
215
|
+
.join('users', 'orders.user_id', 'users.id')
|
|
216
|
+
.where('orders.status', '=', 'completed')
|
|
217
|
+
.whereIn('orders.currency', ['USD', 'EUR'])
|
|
218
|
+
.orderBy('orders.created_at', 'DESC')
|
|
219
|
+
.paginate(1, 15); // Returns { data, total, page, perPage, lastPage }
|
|
220
|
+
|
|
221
|
+
// Aggregates
|
|
222
|
+
const totalRevenue = await DB.table('orders').sum('total_amount');
|
|
223
|
+
const orderCount = await DB.table('orders').where('status', 'pending').count();
|
|
224
|
+
|
|
225
|
+
// Direct Insert, Update, Delete
|
|
226
|
+
await DB.table('logs').insert({ event: 'login', ip: '127.0.0.1' });
|
|
227
|
+
await DB.table('users').where('id', 5).update({ status: 'active' });
|
|
228
|
+
await DB.table('sessions').where('expires_at', '<', new Date()).delete();
|
|
229
|
+
|
|
230
|
+
// Raw SQL & Transactions
|
|
231
|
+
const rawRows = await DB.query('SELECT * FROM users WHERE email = ?', ['admin@dev.com']);
|
|
232
|
+
|
|
233
|
+
await DB.transaction(async (trx) => {
|
|
234
|
+
await trx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
|
|
235
|
+
await trx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 4.3 Database Migrations (`database/migrations/`)
|
|
240
|
+
|
|
241
|
+
Migrations define schema changes using `TableBlueprint`:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { Schema, type Migration, type TableBlueprint } from 'aerojs';
|
|
245
|
+
|
|
246
|
+
export default class CreateUsersTable implements Migration {
|
|
247
|
+
public name = '2026_09_30_000000_create_users_table';
|
|
248
|
+
|
|
249
|
+
public async up(): Promise<void> {
|
|
250
|
+
await Schema.create('users', (table: TableBlueprint) => {
|
|
251
|
+
table.increments('id');
|
|
252
|
+
table.string('name', 255).notNull();
|
|
253
|
+
table.string('email', 191).notNull().unique();
|
|
254
|
+
table.string('role').defaultTo('user');
|
|
255
|
+
table.boolean('is_active').defaultTo(true);
|
|
256
|
+
table.text('bio').nullable();
|
|
257
|
+
table.timestamps(); // Creates created_at and updated_at
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async down(): Promise<void> {
|
|
262
|
+
await Schema.dropIfExists('users');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Blueprint Column Methods
|
|
268
|
+
- `table.increments('id')`
|
|
269
|
+
- `table.string('name', 255)`
|
|
270
|
+
- `table.integer('age')`
|
|
271
|
+
- `table.boolean('is_verified')`
|
|
272
|
+
- `table.text('content')`
|
|
273
|
+
- `table.timestamp('published_at')`
|
|
274
|
+
- `table.timestamps()`
|
|
275
|
+
- `.nullable()`, `.notNull()`, `.unique()`, `.defaultTo(val)`
|
|
276
|
+
|
|
277
|
+
### 4.4 Database Adapters, Dialects & Connection Pooling
|
|
278
|
+
|
|
279
|
+
AeroJS supports multiple relational databases via Knex connection pooling, as well as native zero-dependency in-memory execution.
|
|
280
|
+
|
|
281
|
+
#### Environment Variables (`.env`):
|
|
282
|
+
```env
|
|
283
|
+
# Database (MySQL / PostgreSQL / SQLite)
|
|
284
|
+
DB_CONNECTION=mysql
|
|
285
|
+
DB_HOST=127.0.0.1
|
|
286
|
+
DB_PORT=3306
|
|
287
|
+
DB_USER=root
|
|
288
|
+
DB_PASSWORD=
|
|
289
|
+
DB_DATABASE=aerojs_app
|
|
290
|
+
DB_POOL_MIN=2
|
|
291
|
+
DB_POOL_MAX=20
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### How Dialects & Pooling Work in `config/database.ts`:
|
|
295
|
+
```typescript
|
|
296
|
+
const poolMin = parseInt(process.env.DB_POOL_MIN || '2', 10);
|
|
297
|
+
const poolMax = parseInt(process.env.DB_POOL_MAX || '20', 10);
|
|
298
|
+
|
|
299
|
+
export const databaseConfig = {
|
|
300
|
+
default: (process.env.DB_CONNECTION || 'mysql').toLowerCase(),
|
|
301
|
+
pool: { min: poolMin, max: poolMax },
|
|
302
|
+
connections: {
|
|
303
|
+
mysql: { client: 'mysql2', ... },
|
|
304
|
+
postgres: { client: 'pg', ... },
|
|
305
|
+
sqlite: { client: 'better-sqlite3', ... },
|
|
306
|
+
memory: { client: 'memory' },
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### 1. Using MySQL (`client: 'mysql2'`)
|
|
312
|
+
```typescript
|
|
313
|
+
import knex from 'knex';
|
|
314
|
+
import { useKnex } from 'aerojs';
|
|
315
|
+
import { databaseConfig } from './config/database.js';
|
|
316
|
+
|
|
317
|
+
const db = knex({
|
|
318
|
+
client: 'mysql2',
|
|
319
|
+
connection: databaseConfig.connections.mysql,
|
|
320
|
+
pool: databaseConfig.pool,
|
|
321
|
+
});
|
|
322
|
+
useKnex(db); // Seamlessly binds to all Models and DB.table()
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### 2. Using PostgreSQL (`client: 'pg'`)
|
|
326
|
+
Change `.env` to `DB_CONNECTION=postgres`, `DB_PORT=5432`, `DB_USER=postgres`:
|
|
327
|
+
```typescript
|
|
328
|
+
import knex from 'knex';
|
|
329
|
+
import { useKnex } from 'aerojs';
|
|
330
|
+
import { databaseConfig } from './config/database.js';
|
|
331
|
+
|
|
332
|
+
const db = knex({
|
|
333
|
+
client: 'pg',
|
|
334
|
+
connection: databaseConfig.connections.postgres,
|
|
335
|
+
pool: databaseConfig.pool,
|
|
336
|
+
});
|
|
337
|
+
useKnex(db);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### 3. Using SQLite (`client: 'better-sqlite3'`)
|
|
341
|
+
Change `.env` to `DB_CONNECTION=sqlite`, `DB_DATABASE=storage/database.sqlite`:
|
|
342
|
+
```typescript
|
|
343
|
+
import knex from 'knex';
|
|
344
|
+
import { useKnex } from 'aerojs';
|
|
345
|
+
import { databaseConfig } from './config/database.js';
|
|
346
|
+
|
|
347
|
+
const db = knex({
|
|
348
|
+
client: 'better-sqlite3',
|
|
349
|
+
connection: databaseConfig.connections.sqlite,
|
|
350
|
+
useNullAsDefault: true,
|
|
351
|
+
});
|
|
352
|
+
useKnex(db);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### 4. Connecting Prisma or Drizzle
|
|
356
|
+
```typescript
|
|
357
|
+
import { usePrisma, useDrizzle } from 'aerojs';
|
|
358
|
+
|
|
359
|
+
// Prisma
|
|
360
|
+
usePrisma(prismaClient);
|
|
361
|
+
|
|
362
|
+
// Drizzle
|
|
363
|
+
useDrizzle(drizzleDb);
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 5. Frontend & View Engines (Vue, React, Edge.js, EJS)
|
|
369
|
+
|
|
370
|
+
AeroJS supports multiple presentation tiers:
|
|
371
|
+
|
|
372
|
+
### 5.1 Inertia.js (React & Vue 3)
|
|
373
|
+
|
|
374
|
+
For modern Single Page Applications with server-side routing:
|
|
375
|
+
|
|
376
|
+
1. **Enable in `server.ts`**:
|
|
377
|
+
```typescript
|
|
378
|
+
app.useInertia({
|
|
379
|
+
rootView: 'default', // Or custom HTML root template with vite() tags
|
|
380
|
+
version: '1.0',
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
2. **Render in Controller**:
|
|
385
|
+
```typescript
|
|
386
|
+
export class UserController {
|
|
387
|
+
public async index(ctx: AeroContext): Promise<void> {
|
|
388
|
+
const users = await User.all();
|
|
389
|
+
await ctx.inertia.render('Users/Index', {
|
|
390
|
+
users,
|
|
391
|
+
title: 'User Management',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### 5.2 Official Vite Integration (`vite.config.ts` for React & Vue 3)
|
|
398
|
+
|
|
399
|
+
AeroJS integrates seamlessly with **Vite** for ultra-fast Hot Module Replacement (HMR) during development and optimized asset hashing in production.
|
|
400
|
+
|
|
401
|
+
#### 1. Directory Structure:
|
|
402
|
+
```text
|
|
403
|
+
my-aero-app/
|
|
404
|
+
├── resources/
|
|
405
|
+
│ ├── js/
|
|
406
|
+
│ │ └── app.ts # Vue 3 / React entry point
|
|
407
|
+
│ └── css/
|
|
408
|
+
│ └── app.css # Tailwind CSS / Styles
|
|
409
|
+
├── public/
|
|
410
|
+
│ └── build/ # Generated production assets & manifest.json
|
|
411
|
+
└── vite.config.ts # Vite configuration
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### 2. `vite.config.ts` Configuration:
|
|
415
|
+
```typescript
|
|
416
|
+
import { defineConfig } from 'vite';
|
|
417
|
+
// For React: import react from '@vitejs/plugin-react';
|
|
418
|
+
// For Vue 3: import vue from '@vitejs/plugin-vue';
|
|
419
|
+
|
|
420
|
+
export default defineConfig({
|
|
421
|
+
plugins: [
|
|
422
|
+
// react(), // or vue()
|
|
423
|
+
],
|
|
424
|
+
build: {
|
|
425
|
+
outDir: 'public/build',
|
|
426
|
+
manifest: true, // Generates manifest.json with hashed chunks
|
|
427
|
+
rollupOptions: {
|
|
428
|
+
input: 'resources/js/app.ts', // or resources/js/app.tsx
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
server: {
|
|
432
|
+
cors: true,
|
|
433
|
+
port: 5173,
|
|
434
|
+
strictPort: true,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### 3. Injecting Vite Assets with AeroJS `vite()` Helper:
|
|
440
|
+
AeroJS provides a built-in `vite(entry)` helper that auto-detects development vs production:
|
|
441
|
+
- In Development: Injects `<script type="module" src="http://localhost:5173/@vite/client">` + entry scripts.
|
|
442
|
+
- In Production: Automatically reads `public/build/manifest.json` and injects hashed `<link rel="stylesheet">` and `<script>` tags.
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import { vite } from 'aerojs';
|
|
446
|
+
|
|
447
|
+
// In your root Inertia view or HTML layout:
|
|
448
|
+
app.useInertia({
|
|
449
|
+
rootView: `
|
|
450
|
+
<!DOCTYPE html>
|
|
451
|
+
<html lang="en">
|
|
452
|
+
<head>
|
|
453
|
+
<meta charset="UTF-8">
|
|
454
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
455
|
+
${vite('resources/js/app.ts')}
|
|
456
|
+
</head>
|
|
457
|
+
<body>
|
|
458
|
+
@inertia
|
|
459
|
+
</body>
|
|
460
|
+
</html>
|
|
461
|
+
`,
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
#### 4. NPM Scripts (`package.json`):
|
|
466
|
+
```json
|
|
467
|
+
{
|
|
468
|
+
"scripts": {
|
|
469
|
+
"dev": "concurrently \"tsx watch server.ts\" \"vite\"",
|
|
470
|
+
"build": "tsc && vite build",
|
|
471
|
+
"start": "node dist/server.js"
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### 5.3 Edge.js Template Engine (AdonisJS Style)
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import { Edge } from 'edge.js';
|
|
480
|
+
import { createEdgeDriver } from 'aerojs';
|
|
481
|
+
|
|
482
|
+
const edge = new Edge();
|
|
483
|
+
edge.mount(new URL('./views', import.meta.url));
|
|
484
|
+
app.useViewEngine(createEdgeDriver(edge));
|
|
485
|
+
|
|
486
|
+
// In Route / Controller:
|
|
487
|
+
router.get('/dashboard', async (ctx) => {
|
|
488
|
+
await ctx.view('dashboard', { user: ctx.state.user });
|
|
489
|
+
});
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 5.3 EJS Template Engine
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import ejs from 'ejs';
|
|
496
|
+
import { createEjsDriver } from 'aerojs';
|
|
497
|
+
|
|
498
|
+
app.useViewEngine(createEjsDriver(ejs));
|
|
499
|
+
|
|
500
|
+
// In Route / Controller:
|
|
501
|
+
router.get('/about', async (ctx) => {
|
|
502
|
+
await ctx.view('pages/about.ejs', { title: 'About Us' });
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 5.4 SSR Engine
|
|
507
|
+
|
|
508
|
+
AeroJS includes a built-in `SSREngine` supporting asynchronous streaming and string rendering for server-rendered React or Vue components.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 6. Request Validation (`app/validators/`)
|
|
513
|
+
|
|
514
|
+
Always validate request payloads before they reach business logic:
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
export const createUserSchema = {
|
|
518
|
+
body: {
|
|
519
|
+
type: 'object',
|
|
520
|
+
required: ['name', 'email'],
|
|
521
|
+
properties: {
|
|
522
|
+
name: { type: 'string', minLength: 2, maxLength: 100 },
|
|
523
|
+
email: { type: 'string', format: 'email' },
|
|
524
|
+
role: { type: 'string', enum: ['admin', 'developer', 'user'] },
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Attach to routes using `.schema(createUserSchema)`. AeroJS will automatically reject invalid requests with status `422 Unprocessable Entity`.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## 7. Background Queue Jobs & Mail
|
|
535
|
+
|
|
536
|
+
### 7.1 Jobs (`app/jobs/`)
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { Job, Queue } from 'aerojs';
|
|
540
|
+
|
|
541
|
+
export class SendWelcomeEmailJob extends Job {
|
|
542
|
+
public static override queue = 'emails';
|
|
543
|
+
public static override maxTries = 3;
|
|
544
|
+
|
|
545
|
+
public async handle(): Promise<void> {
|
|
546
|
+
const { email, name } = this.data;
|
|
547
|
+
console.log(`Sending welcome email to ${name} (${email})`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Dispatching a Job:
|
|
552
|
+
await Queue.push(new SendWelcomeEmailJob({ email: 'user@aerojs.dev', name: 'User' }));
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### 7.2 Mail Sending
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
import { Mail } from 'aerojs';
|
|
559
|
+
|
|
560
|
+
await Mail.send({
|
|
561
|
+
to: 'user@example.com',
|
|
562
|
+
subject: 'Welcome to our platform',
|
|
563
|
+
html: '<h1>Welcome!</h1><p>Your account is now ready.</p>',
|
|
564
|
+
});
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## 8. Testing Conventions (`tests/`)
|
|
570
|
+
|
|
571
|
+
Use AeroJS `createTestClient` with Vitest for fast, in-memory end-to-end testing without opening real network sockets:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import { describe, it, expect } from 'vitest';
|
|
575
|
+
import { createTestClient } from 'aerojs/testing';
|
|
576
|
+
import app from '../server.js';
|
|
577
|
+
|
|
578
|
+
describe('User API Tests', () => {
|
|
579
|
+
const client = createTestClient(app);
|
|
580
|
+
|
|
581
|
+
it('GET /api/users returns list of users', async () => {
|
|
582
|
+
const res = await client.get('/api/users');
|
|
583
|
+
expect(res.status).toBe(200);
|
|
584
|
+
expect(res.json().success).toBe(true);
|
|
585
|
+
expect(Array.isArray(res.json().data)).toBe(true);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('POST /api/users validates payload', async () => {
|
|
589
|
+
const res = await client.post('/api/users', {
|
|
590
|
+
body: { name: 'A' }, // Missing email & name too short
|
|
591
|
+
});
|
|
592
|
+
expect(res.status).toBe(422);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### 8.2 Next.js Style Interactive Error Dashboard
|
|
598
|
+
|
|
599
|
+
In development mode (`debug: true` or `NODE_ENV !== 'production'`):
|
|
600
|
+
- **Browser Requests (`Accept: text/html`)**: AeroJS intercepts unhandled exceptions and renders a rich dark-mode Error Dashboard. It parses the stack trace, extracts the local source code file, and highlights the exact crashing line with an error indicator (`→`). It also provides interactive call stack inspection, request headers/parameters explorer, and system diagnostics.
|
|
601
|
+
- **API Requests (`Accept: application/json`)**: AeroJS returns clean JSON with `{ error: { message, status, code, stack } }`.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## 9. Frontend & View Engines (Edge.js, EJS, React, Vue 3)
|
|
606
|
+
|
|
607
|
+
AeroJS supports both **Server-Side Template Engines (MPA)** and **Modern Single-Page Applications (SPA)** via Inertia.js protocol.
|
|
608
|
+
|
|
609
|
+
### 9.1 Edge.js (AdonisJS Official Template Engine)
|
|
610
|
+
- **Install**: `npm install edge.js`
|
|
611
|
+
- **Location**: `views/edge/*.edge`
|
|
612
|
+
- **Syntax**:
|
|
613
|
+
```edge
|
|
614
|
+
@each(product in products)
|
|
615
|
+
<div class="product-card">
|
|
616
|
+
<h3>{{ product.name }}</h3>
|
|
617
|
+
<span>${{ product.price }}</span>
|
|
618
|
+
@if(product.stock > 10)
|
|
619
|
+
<span class="stock-ok">In Stock ({{ product.stock }})</span>
|
|
620
|
+
@else
|
|
621
|
+
<span class="stock-low">Low Stock ({{ product.stock }})</span>
|
|
622
|
+
@endif
|
|
623
|
+
</div>
|
|
624
|
+
@endeach
|
|
625
|
+
```
|
|
626
|
+
- **Handler**:
|
|
627
|
+
```ts
|
|
628
|
+
import { renderEdge } from '../app/views/engine.js';
|
|
629
|
+
router.get('/views/edge', async (ctx: AeroContext) => {
|
|
630
|
+
const products = await DB.table('products').get();
|
|
631
|
+
const html = await renderEdge('edge/products', { products });
|
|
632
|
+
ctx.html(html);
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### 9.2 EJS (Embedded JavaScript)
|
|
637
|
+
- **Install**: `npm install ejs && npm install -D @types/ejs`
|
|
638
|
+
- **Location**: `views/ejs/*.ejs`
|
|
639
|
+
- **Syntax**:
|
|
640
|
+
```ejs
|
|
641
|
+
<% products.forEach(function(product) { %>
|
|
642
|
+
<div class="product-card">
|
|
643
|
+
<h3><%= product.name %></h3>
|
|
644
|
+
<span>$<%= product.price %></span>
|
|
645
|
+
<% if (product.stock > 10) { %>
|
|
646
|
+
<span class="stock-ok">In Stock</span>
|
|
647
|
+
<% } else { %>
|
|
648
|
+
<span class="stock-low">Low Stock</span>
|
|
649
|
+
<% } %>
|
|
650
|
+
</div>
|
|
651
|
+
<% }); %>
|
|
652
|
+
```
|
|
653
|
+
- **Handler**:
|
|
654
|
+
```ts
|
|
655
|
+
import { renderEjs } from '../app/views/engine.js';
|
|
656
|
+
router.get('/views/ejs', async (ctx: AeroContext) => {
|
|
657
|
+
const products = await DB.table('products').get();
|
|
658
|
+
const html = await renderEjs('ejs/products.ejs', { products });
|
|
659
|
+
ctx.html(html);
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### 9.3 React 18 (Inertia.js SPA)
|
|
664
|
+
- **Install**: `@inertiajs/react react react-dom`
|
|
665
|
+
- **Location**: `resources/js/Pages/ProductsReact.tsx`
|
|
666
|
+
- **Handler**:
|
|
667
|
+
```ts
|
|
668
|
+
router.get('/views/react', async (ctx: AeroContext) => {
|
|
669
|
+
const products = await DB.table('products').get();
|
|
670
|
+
await ctx.inertia.render('ProductsReact', { products });
|
|
671
|
+
});
|
|
672
|
+
```
|
|
673
|
+
- **Protocol**:
|
|
674
|
+
- Initial visit from browser: Returns HTML shell with `<div id="app" data-page='{"component":"ProductsReact","props":{...}}'></div>`.
|
|
675
|
+
- AJAX visit with `X-Inertia: true`: Returns lightweight JSON props.
|
|
676
|
+
|
|
677
|
+
### 9.4 Vue 3 (Inertia.js SPA)
|
|
678
|
+
- **Install**: `@inertiajs/vue3 vue`
|
|
679
|
+
- **Location**: `resources/js/Pages/ProductsVue.vue`
|
|
680
|
+
- **Handler**:
|
|
681
|
+
```ts
|
|
682
|
+
router.get('/views/vue', async (ctx: AeroContext) => {
|
|
683
|
+
const products = await DB.table('products').get();
|
|
684
|
+
await ctx.inertia.render('ProductsVue', { products });
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## 10. AI Agent Implementation Checklist
|
|
691
|
+
|
|
692
|
+
When asked to build or modify any feature in an AeroJS project:
|
|
693
|
+
|
|
694
|
+
1. **New Route**: Register in `routes/api.ts` or `routes/web.ts` using `router.group()` and controller tuples `[Controller, 'action']`.
|
|
695
|
+
2. **New Controller**: Place in `app/controllers/`. Keep it lean; forward data to `app/services/`.
|
|
696
|
+
3. **New Business Logic**: Place in `app/services/`.
|
|
697
|
+
4. **New Entity / Table**:
|
|
698
|
+
- Create migration in `database/migrations/` using `Schema.create('table_name', (table) => ...)`.
|
|
699
|
+
- Create model in `app/models/` extending `Model`.
|
|
700
|
+
5. **New Input Validation**: Create schema in `app/validators/` and attach via `.schema(...)`.
|
|
701
|
+
6. **Async Tasks / Emails**: Create job in `app/jobs/` extending `Job` and dispatch via `Queue.push()`.
|
|
702
|
+
7. **Frontend Views**:
|
|
703
|
+
- Use Edge.js (`views/edge/`) or EJS (`views/ejs/`) for Server-Rendered HTML.
|
|
704
|
+
- Use Inertia.js React (`resources/js/Pages/*.tsx`) or Vue 3 (`resources/js/Pages/*.vue`) for SPAs.
|
|
705
|
+
8. **Verification**: Always verify changes by running:
|
|
706
|
+
```bash
|
|
707
|
+
npm run build
|
|
708
|
+
npm test
|
|
709
|
+
```
|