@orchestr-sh/orchestr 1.5.10 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +433 -936
- package/dist/Console/Command.d.ts +59 -0
- package/dist/Console/Command.d.ts.map +1 -0
- package/dist/Console/Command.js +68 -0
- package/dist/Console/Command.js.map +1 -0
- package/dist/Console/Commands/MakeMigrationCommand.d.ts +15 -0
- package/dist/Console/Commands/MakeMigrationCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MakeMigrationCommand.js +41 -0
- package/dist/Console/Commands/MakeMigrationCommand.js.map +1 -0
- package/dist/Console/Commands/MakeSeederCommand.d.ts +23 -0
- package/dist/Console/Commands/MakeSeederCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MakeSeederCommand.js +109 -0
- package/dist/Console/Commands/MakeSeederCommand.js.map +1 -0
- package/dist/Console/Commands/MigrateCommand.d.ts +15 -0
- package/dist/Console/Commands/MigrateCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MigrateCommand.js +48 -0
- package/dist/Console/Commands/MigrateCommand.js.map +1 -0
- package/dist/Console/Commands/MigrateFreshCommand.d.ts +15 -0
- package/dist/Console/Commands/MigrateFreshCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MigrateFreshCommand.js +54 -0
- package/dist/Console/Commands/MigrateFreshCommand.js.map +1 -0
- package/dist/Console/Commands/MigrateRefreshCommand.d.ts +15 -0
- package/dist/Console/Commands/MigrateRefreshCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MigrateRefreshCommand.js +52 -0
- package/dist/Console/Commands/MigrateRefreshCommand.js.map +1 -0
- package/dist/Console/Commands/MigrateResetCommand.d.ts +15 -0
- package/dist/Console/Commands/MigrateResetCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MigrateResetCommand.js +46 -0
- package/dist/Console/Commands/MigrateResetCommand.js.map +1 -0
- package/dist/Console/Commands/MigrateRollbackCommand.d.ts +15 -0
- package/dist/Console/Commands/MigrateRollbackCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MigrateRollbackCommand.js +49 -0
- package/dist/Console/Commands/MigrateRollbackCommand.js.map +1 -0
- package/dist/Console/Commands/MigrateStatusCommand.d.ts +15 -0
- package/dist/Console/Commands/MigrateStatusCommand.d.ts.map +1 -0
- package/dist/Console/Commands/MigrateStatusCommand.js +58 -0
- package/dist/Console/Commands/MigrateStatusCommand.js.map +1 -0
- package/dist/Console/Commands/SeedCommand.d.ts +15 -0
- package/dist/Console/Commands/SeedCommand.d.ts.map +1 -0
- package/dist/Console/Commands/SeedCommand.js +44 -0
- package/dist/Console/Commands/SeedCommand.js.map +1 -0
- package/dist/Console/ConsoleKernel.d.ts +45 -0
- package/dist/Console/ConsoleKernel.d.ts.map +1 -0
- package/dist/Console/ConsoleKernel.js +109 -0
- package/dist/Console/ConsoleKernel.js.map +1 -0
- package/dist/Console/orchestr.d.ts +8 -0
- package/dist/Console/orchestr.d.ts.map +1 -0
- package/dist/Console/orchestr.js +45 -0
- package/dist/Console/orchestr.js.map +1 -0
- package/dist/Database/Ensemble/Concerns/HasDynamicRelations.d.ts +5 -0
- package/dist/Database/Ensemble/Concerns/HasDynamicRelations.d.ts.map +1 -1
- package/dist/Database/Ensemble/Concerns/HasDynamicRelations.js.map +1 -1
- package/dist/Database/Ensemble/Concerns/HasRelationships.d.ts +20 -0
- package/dist/Database/Ensemble/Concerns/HasRelationships.d.ts.map +1 -1
- package/dist/Database/Ensemble/Concerns/HasRelationships.js +57 -0
- package/dist/Database/Ensemble/Concerns/HasRelationships.js.map +1 -1
- package/dist/Database/Ensemble/Relations/MorphMany.d.ts +97 -0
- package/dist/Database/Ensemble/Relations/MorphMany.d.ts.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphMany.js +189 -0
- package/dist/Database/Ensemble/Relations/MorphMany.js.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphMap.d.ts +59 -0
- package/dist/Database/Ensemble/Relations/MorphMap.d.ts.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphMap.js +84 -0
- package/dist/Database/Ensemble/Relations/MorphMap.js.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphOne.d.ts +93 -0
- package/dist/Database/Ensemble/Relations/MorphOne.d.ts.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphOne.js +179 -0
- package/dist/Database/Ensemble/Relations/MorphOne.js.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphTo.d.ts +98 -0
- package/dist/Database/Ensemble/Relations/MorphTo.d.ts.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphTo.js +214 -0
- package/dist/Database/Ensemble/Relations/MorphTo.js.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphToMany.d.ts +73 -0
- package/dist/Database/Ensemble/Relations/MorphToMany.d.ts.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphToMany.js +164 -0
- package/dist/Database/Ensemble/Relations/MorphToMany.js.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphedByMany.d.ts +29 -0
- package/dist/Database/Ensemble/Relations/MorphedByMany.d.ts.map +1 -0
- package/dist/Database/Ensemble/Relations/MorphedByMany.js +58 -0
- package/dist/Database/Ensemble/Relations/MorphedByMany.js.map +1 -0
- package/dist/Database/Ensemble/Relations/index.d.ts +6 -0
- package/dist/Database/Ensemble/Relations/index.d.ts.map +1 -1
- package/dist/Database/Ensemble/Relations/index.js +14 -1
- package/dist/Database/Ensemble/Relations/index.js.map +1 -1
- package/dist/Database/Migrations/Blueprint.d.ts +166 -0
- package/dist/Database/Migrations/Blueprint.d.ts.map +1 -0
- package/dist/Database/Migrations/Blueprint.js +318 -0
- package/dist/Database/Migrations/Blueprint.js.map +1 -0
- package/dist/Database/Migrations/ColumnDefinition.d.ts +62 -0
- package/dist/Database/Migrations/ColumnDefinition.d.ts.map +1 -0
- package/dist/Database/Migrations/ColumnDefinition.js +95 -0
- package/dist/Database/Migrations/ColumnDefinition.js.map +1 -0
- package/dist/Database/Migrations/ForeignKeyDefinition.d.ts +32 -0
- package/dist/Database/Migrations/ForeignKeyDefinition.d.ts.map +1 -0
- package/dist/Database/Migrations/ForeignKeyDefinition.js +50 -0
- package/dist/Database/Migrations/ForeignKeyDefinition.js.map +1 -0
- package/dist/Database/Migrations/Migration.d.ts +17 -0
- package/dist/Database/Migrations/Migration.d.ts.map +1 -0
- package/dist/Database/Migrations/Migration.js +12 -0
- package/dist/Database/Migrations/Migration.js.map +1 -0
- package/dist/Database/Migrations/MigrationCreator.d.ts +42 -0
- package/dist/Database/Migrations/MigrationCreator.d.ts.map +1 -0
- package/dist/Database/Migrations/MigrationCreator.js +184 -0
- package/dist/Database/Migrations/MigrationCreator.js.map +1 -0
- package/dist/Database/Migrations/MigrationRepository.d.ts +65 -0
- package/dist/Database/Migrations/MigrationRepository.d.ts.map +1 -0
- package/dist/Database/Migrations/MigrationRepository.js +135 -0
- package/dist/Database/Migrations/MigrationRepository.js.map +1 -0
- package/dist/Database/Migrations/Migrator.d.ts +84 -0
- package/dist/Database/Migrations/Migrator.d.ts.map +1 -0
- package/dist/Database/Migrations/Migrator.js +239 -0
- package/dist/Database/Migrations/Migrator.js.map +1 -0
- package/dist/Database/Migrations/SchemaBuilder.d.ts +136 -0
- package/dist/Database/Migrations/SchemaBuilder.d.ts.map +1 -0
- package/dist/Database/Migrations/SchemaBuilder.js +515 -0
- package/dist/Database/Migrations/SchemaBuilder.js.map +1 -0
- package/dist/Database/Seeders/Seeder.d.ts +29 -0
- package/dist/Database/Seeders/Seeder.d.ts.map +1 -0
- package/dist/Database/Seeders/Seeder.js +41 -0
- package/dist/Database/Seeders/Seeder.js.map +1 -0
- package/dist/Database/Seeders/SeederRunner.d.ts +41 -0
- package/dist/Database/Seeders/SeederRunner.d.ts.map +1 -0
- package/dist/Database/Seeders/SeederRunner.js +134 -0
- package/dist/Database/Seeders/SeederRunner.js.map +1 -0
- package/dist/index.d.ts +27 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,1127 +1,624 @@
|
|
|
1
1
|
# Orchestr
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
Built from the ground up with Laravel's core components:
|
|
8
|
-
|
|
9
|
-
- **Service Container** - Full IoC container with dependency injection and reflection
|
|
10
|
-
- **Service Providers** - Bootstrap and register services
|
|
11
|
-
- **Configuration** - Dot-notation config repository with runtime access
|
|
12
|
-
- **HTTP Router** - Laravel-style routing with parameter binding and file-based route loading
|
|
13
|
-
- **Request/Response** - Elegant HTTP abstractions
|
|
14
|
-
- **Middleware** - Global and route-level middleware pipeline
|
|
15
|
-
- **Controllers** - MVC architecture support
|
|
16
|
-
- **FormRequest** - Laravel-style validation and authorization
|
|
17
|
-
- **Facades** - Static proxy access to services (Route, DB)
|
|
18
|
-
- **Query Builder** - Fluent database query builder with full Laravel API
|
|
19
|
-
- **Ensemble ORM** - ActiveRecord ORM (Laravel's Eloquent equivalent) with relationships (HasOne, HasMany, BelongsTo, BelongsToMany), eager/lazy loading, soft deletes, and more
|
|
20
|
-
- **Database Manager** - Multi-connection database management
|
|
21
|
-
- **Application Lifecycle** - Complete Laravel bootstrap process
|
|
3
|
+
A Laravel-inspired ORM and framework for TypeScript. Write elegant backend applications with ActiveRecord models (called Ensembles), relationships, query building, and more.
|
|
22
4
|
|
|
23
5
|
## Installation
|
|
24
6
|
|
|
25
7
|
```bash
|
|
26
|
-
npm install @orchestr-sh/orchestr reflect-metadata
|
|
8
|
+
npm install @orchestr-sh/orchestr reflect-metadata drizzle-orm
|
|
9
|
+
npm install better-sqlite3 # or your preferred database driver
|
|
27
10
|
```
|
|
28
11
|
|
|
29
|
-
**Note**: `reflect-metadata` is required for dependency injection to work.
|
|
30
|
-
|
|
31
12
|
## Quick Start
|
|
32
13
|
|
|
33
14
|
```typescript
|
|
34
|
-
import 'reflect-metadata';
|
|
35
|
-
import { Application, Kernel,
|
|
15
|
+
import 'reflect-metadata';
|
|
16
|
+
import { Application, Kernel, ConfigServiceProvider, Route } from '@orchestr-sh/orchestr';
|
|
36
17
|
|
|
37
|
-
|
|
38
|
-
const app = new Application(__dirname);
|
|
18
|
+
const app = new Application(process.cwd());
|
|
39
19
|
|
|
40
|
-
//
|
|
41
|
-
app.register(
|
|
42
|
-
|
|
20
|
+
// Configure database
|
|
21
|
+
app.register(new ConfigServiceProvider(app, {
|
|
22
|
+
database: {
|
|
23
|
+
default: 'sqlite',
|
|
24
|
+
connections: {
|
|
25
|
+
sqlite: {
|
|
26
|
+
adapter: 'drizzle',
|
|
27
|
+
driver: 'sqlite',
|
|
28
|
+
database: './database.db',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
const kernel = new Kernel(app);
|
|
34
|
+
await app.boot();
|
|
46
35
|
|
|
47
36
|
// Define routes
|
|
48
37
|
Route.get('/', async (req, res) => {
|
|
49
|
-
return res.json({ message: '
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
Route.get('/users/:id', async (req, res) => {
|
|
53
|
-
const id = req.routeParam('id');
|
|
54
|
-
return res.json({ user: { id, name: 'John Doe' } });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
Route.post('/users', async (req, res) => {
|
|
58
|
-
const data = req.only(['name', 'email']);
|
|
59
|
-
return res.status(201).json({ user: data });
|
|
38
|
+
return res.json({ message: 'Welcome to Orchestr!' });
|
|
60
39
|
});
|
|
61
40
|
|
|
62
41
|
// Start server
|
|
42
|
+
const kernel = new Kernel(app);
|
|
63
43
|
kernel.listen(3000);
|
|
64
44
|
```
|
|
65
45
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
### Configuration
|
|
46
|
+
## Models (Ensembles)
|
|
69
47
|
|
|
70
|
-
|
|
48
|
+
Ensembles are ActiveRecord models with a fluent API for querying and relationships.
|
|
71
49
|
|
|
72
50
|
```typescript
|
|
73
|
-
import {
|
|
51
|
+
import { Ensemble } from '@orchestr-sh/orchestr';
|
|
74
52
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
name: 'My Application',
|
|
81
|
-
env: 'production',
|
|
82
|
-
debug: false,
|
|
83
|
-
url: 'http://localhost:3000',
|
|
84
|
-
},
|
|
85
|
-
database: {
|
|
86
|
-
default: 'mysql',
|
|
87
|
-
connections: {
|
|
88
|
-
mysql: {
|
|
89
|
-
host: 'localhost',
|
|
90
|
-
port: 3306,
|
|
91
|
-
database: 'mydb',
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}));
|
|
96
|
-
|
|
97
|
-
await app.boot();
|
|
98
|
-
|
|
99
|
-
// Access via container
|
|
100
|
-
const configInstance = app.make('config');
|
|
101
|
-
const appName = configInstance.get('app.name'); // 'My Application'
|
|
102
|
-
const dbHost = configInstance.get('database.connections.mysql.host'); // 'localhost'
|
|
103
|
-
|
|
104
|
-
// Access via facade
|
|
105
|
-
const name = Config.get('app.name');
|
|
106
|
-
const debug = Config.get('app.debug', false); // with default
|
|
53
|
+
export class User extends Ensemble {
|
|
54
|
+
protected table = 'users';
|
|
55
|
+
protected fillable = ['name', 'email', 'password'];
|
|
56
|
+
protected hidden = ['password'];
|
|
57
|
+
}
|
|
107
58
|
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
const
|
|
59
|
+
// Query
|
|
60
|
+
const users = await User.query().where('active', true).get();
|
|
61
|
+
const user = await User.find(1);
|
|
111
62
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
Config.set({
|
|
115
|
-
'app.timezone': 'UTC',
|
|
116
|
-
'app.locale': 'en'
|
|
117
|
-
});
|
|
63
|
+
// Create
|
|
64
|
+
const user = await User.create({ name: 'John', email: 'john@example.com' });
|
|
118
65
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
66
|
+
// Update
|
|
67
|
+
user.name = 'Jane';
|
|
68
|
+
await user.save();
|
|
123
69
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
Config.prepend('app.middleware', 'LoggingMiddleware');
|
|
70
|
+
// Delete
|
|
71
|
+
await user.delete();
|
|
127
72
|
```
|
|
128
73
|
|
|
129
|
-
|
|
74
|
+
## Querying
|
|
130
75
|
|
|
131
|
-
|
|
76
|
+
Fluent query builder with chainable methods.
|
|
132
77
|
|
|
133
78
|
```typescript
|
|
134
|
-
|
|
135
|
-
|
|
79
|
+
import { DB } from '@orchestr-sh/orchestr';
|
|
80
|
+
|
|
81
|
+
// Query builder
|
|
82
|
+
const users = await DB.table('users')
|
|
83
|
+
.where('votes', '>', 100)
|
|
84
|
+
.orderBy('created_at', 'desc')
|
|
85
|
+
.limit(10)
|
|
86
|
+
.get();
|
|
136
87
|
|
|
137
|
-
//
|
|
138
|
-
|
|
88
|
+
// Using models
|
|
89
|
+
const posts = await Post.query()
|
|
90
|
+
.where('published', true)
|
|
91
|
+
.with('author')
|
|
92
|
+
.get();
|
|
139
93
|
|
|
140
|
-
//
|
|
141
|
-
const
|
|
94
|
+
// Aggregates
|
|
95
|
+
const count = await Post.query().count();
|
|
96
|
+
const avg = await Post.query().avg('views');
|
|
142
97
|
```
|
|
143
98
|
|
|
144
|
-
|
|
99
|
+
## Relationships
|
|
145
100
|
|
|
146
|
-
|
|
101
|
+
### Standard Relationships
|
|
147
102
|
|
|
148
103
|
```typescript
|
|
149
|
-
import {
|
|
104
|
+
import { Ensemble, HasMany, BelongsTo, DynamicRelation } from '@orchestr-sh/orchestr';
|
|
105
|
+
|
|
106
|
+
export class User extends Ensemble {
|
|
107
|
+
protected table = 'users';
|
|
150
108
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return [{ id: 1, name: 'John' }];
|
|
109
|
+
@DynamicRelation
|
|
110
|
+
posts(): HasMany<Post, User> {
|
|
111
|
+
return this.hasMany(Post);
|
|
155
112
|
}
|
|
156
113
|
}
|
|
157
114
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
export class UserController extends Controller {
|
|
161
|
-
// Dependencies are automatically injected
|
|
162
|
-
constructor(private userService: UserService) {
|
|
163
|
-
super();
|
|
164
|
-
}
|
|
115
|
+
export class Post extends Ensemble {
|
|
116
|
+
protected table = 'posts';
|
|
165
117
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return
|
|
118
|
+
@DynamicRelation
|
|
119
|
+
user(): BelongsTo<User, this> {
|
|
120
|
+
return this.belongsTo(User);
|
|
169
121
|
}
|
|
170
122
|
}
|
|
171
123
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
this.app.singleton(UserService, () => new UserService());
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
```
|
|
124
|
+
// Use relationships
|
|
125
|
+
const user = await User.find(1);
|
|
126
|
+
const posts = await user.posts().get(); // Query builder
|
|
127
|
+
const posts = await user.posts; // Direct access (via @DynamicRelation)
|
|
180
128
|
|
|
181
|
-
|
|
129
|
+
// Eager loading
|
|
130
|
+
const users = await User.query().with('posts').get();
|
|
182
131
|
|
|
183
|
-
|
|
132
|
+
// Nested eager loading
|
|
133
|
+
const posts = await Post.query().with('user.posts').get();
|
|
134
|
+
```
|
|
184
135
|
|
|
185
|
-
|
|
136
|
+
### Many-to-Many
|
|
186
137
|
|
|
187
138
|
```typescript
|
|
188
|
-
|
|
189
|
-
register(): void {
|
|
190
|
-
this.app.singleton('config', () => ({ /* config */ }));
|
|
191
|
-
}
|
|
139
|
+
import { Ensemble, BelongsToMany, DynamicRelation } from '@orchestr-sh/orchestr';
|
|
192
140
|
|
|
193
|
-
|
|
194
|
-
|
|
141
|
+
export class User extends Ensemble {
|
|
142
|
+
@DynamicRelation
|
|
143
|
+
roles(): BelongsToMany<Role, User> {
|
|
144
|
+
return this.belongsToMany(Role, 'role_user')
|
|
145
|
+
.withPivot('expires_at')
|
|
146
|
+
.withTimestamps();
|
|
195
147
|
}
|
|
196
148
|
}
|
|
197
149
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
### Routing
|
|
202
|
-
|
|
203
|
-
Laravel-style routing with full parameter support:
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
// Simple routes
|
|
207
|
-
Route.get('/users', handler);
|
|
208
|
-
Route.post('/users', handler);
|
|
209
|
-
Route.put('/users/:id', handler);
|
|
210
|
-
Route.delete('/users/:id', handler);
|
|
211
|
-
|
|
212
|
-
// Route parameters
|
|
213
|
-
Route.get('/users/:id/posts/:postId', async (req, res) => {
|
|
214
|
-
const userId = req.routeParam('id');
|
|
215
|
-
const postId = req.routeParam('postId');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Route groups
|
|
219
|
-
Route.group({ prefix: 'api/v1', middleware: authMiddleware }, () => {
|
|
220
|
-
Route.get('/profile', handler);
|
|
221
|
-
Route.post('/posts', handler);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// Named routes
|
|
225
|
-
const route = Route.get('/users', handler);
|
|
226
|
-
route.setName('users.index');
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
#### Loading Routes from Files
|
|
230
|
-
|
|
231
|
-
Organize your routes in separate files, just like Laravel:
|
|
232
|
-
|
|
233
|
-
**routes/web.ts**
|
|
234
|
-
```typescript
|
|
235
|
-
import { Route } from 'orchestr';
|
|
150
|
+
// Attach/Detach
|
|
151
|
+
await user.roles().attach([1, 2, 3]);
|
|
152
|
+
await user.roles().detach([1]);
|
|
236
153
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
154
|
+
// Sync (detach all, attach new)
|
|
155
|
+
await user.roles().sync([1, 2, 3]);
|
|
240
156
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
157
|
+
// Query pivot
|
|
158
|
+
const activeRoles = await user.roles()
|
|
159
|
+
.wherePivot('expires_at', '>', new Date())
|
|
160
|
+
.get();
|
|
244
161
|
```
|
|
245
162
|
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
import { Route } from 'orchestr';
|
|
249
|
-
|
|
250
|
-
Route.group({ prefix: 'api/v1' }, () => {
|
|
251
|
-
Route.get('/users', async (req, res) => {
|
|
252
|
-
return res.json({ users: [] });
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
Route.post('/users', async (req, res) => {
|
|
256
|
-
return res.status(201).json({ created: true });
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
```
|
|
163
|
+
### Polymorphic Relationships
|
|
260
164
|
|
|
261
|
-
**app/Providers/AppRouteServiceProvider.ts**
|
|
262
165
|
```typescript
|
|
263
|
-
import {
|
|
264
|
-
|
|
265
|
-
export class AppRouteServiceProvider extends RouteServiceProvider {
|
|
266
|
-
async boot(): Promise<void> {
|
|
267
|
-
// Load web routes
|
|
268
|
-
this.routes(() => import('../../routes/web'));
|
|
166
|
+
import { Ensemble, MorphMany, MorphTo, DynamicRelation } from '@orchestr-sh/orchestr';
|
|
269
167
|
|
|
270
|
-
|
|
271
|
-
|
|
168
|
+
export class Post extends Ensemble {
|
|
169
|
+
@DynamicRelation
|
|
170
|
+
comments(): MorphMany<Comment, Post> {
|
|
171
|
+
return this.morphMany(Comment, 'commentable');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
272
174
|
|
|
273
|
-
|
|
175
|
+
export class Video extends Ensemble {
|
|
176
|
+
@DynamicRelation
|
|
177
|
+
comments(): MorphMany<Comment, Video> {
|
|
178
|
+
return this.morphMany(Comment, 'commentable');
|
|
274
179
|
}
|
|
275
180
|
}
|
|
276
|
-
```
|
|
277
181
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
182
|
+
export class Comment extends Ensemble {
|
|
183
|
+
@DynamicRelation
|
|
184
|
+
commentable(): MorphTo<Post | Video> {
|
|
185
|
+
return this.morphTo('commentable');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
283
188
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
await
|
|
189
|
+
// Use polymorphic relations
|
|
190
|
+
const post = await Post.find(1);
|
|
191
|
+
const comments = await post.comments;
|
|
287
192
|
|
|
288
|
-
const
|
|
289
|
-
|
|
193
|
+
const comment = await Comment.find(1);
|
|
194
|
+
const parent = await comment.commentable; // Returns Post or Video
|
|
290
195
|
```
|
|
291
196
|
|
|
292
|
-
|
|
197
|
+
## @DynamicRelation Decorator
|
|
293
198
|
|
|
294
|
-
|
|
199
|
+
The `@DynamicRelation` decorator enables dual-mode access to relationships:
|
|
295
200
|
|
|
296
201
|
```typescript
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Route middleware
|
|
304
|
-
const authMiddleware = async (req, res, next) => {
|
|
305
|
-
if (!req.header('authorization')) {
|
|
306
|
-
return res.status(401).json({ message: 'Unauthorized' });
|
|
202
|
+
export class User extends Ensemble {
|
|
203
|
+
@DynamicRelation
|
|
204
|
+
posts(): HasMany<Post, User> {
|
|
205
|
+
return this.hasMany(Post);
|
|
307
206
|
}
|
|
308
|
-
|
|
309
|
-
};
|
|
207
|
+
}
|
|
310
208
|
|
|
311
|
-
|
|
209
|
+
const user = await User.find(1);
|
|
210
|
+
|
|
211
|
+
// Method syntax (returns query builder)
|
|
212
|
+
const query = user.posts();
|
|
213
|
+
const recentPosts = await query.where('created_at', '>', yesterday).get();
|
|
214
|
+
|
|
215
|
+
// Property syntax (returns results directly)
|
|
216
|
+
const allPosts = await user.posts;
|
|
312
217
|
```
|
|
313
218
|
|
|
314
|
-
|
|
219
|
+
Without `@DynamicRelation`, you must always call the method: `user.posts().get()`.
|
|
315
220
|
|
|
316
|
-
|
|
221
|
+
## Migrations
|
|
317
222
|
|
|
318
|
-
|
|
319
|
-
import { FormRequest, ValidationRules, ValidationException, ValidateRequest, Route, Request, Response } from 'orchestr';
|
|
223
|
+
Laravel-style migrations with a fluent Schema builder.
|
|
320
224
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
protected authorize(): boolean {
|
|
325
|
-
return this.request.header('authorization') !== undefined;
|
|
326
|
-
}
|
|
225
|
+
```bash
|
|
226
|
+
# Create a migration
|
|
227
|
+
orchestr make:migration create_users_table --create=users
|
|
327
228
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
name: 'required|string|min:3|max:255',
|
|
332
|
-
email: 'required|email',
|
|
333
|
-
password: 'required|string|min:8|confirmed',
|
|
334
|
-
age: 'numeric|min:18|max:120',
|
|
335
|
-
role: 'required|in:user,admin,moderator',
|
|
336
|
-
};
|
|
337
|
-
}
|
|
229
|
+
# Run migrations
|
|
230
|
+
orchestr migrate
|
|
338
231
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
'name.required': 'Please provide your full name.',
|
|
343
|
-
'email.email': 'Please provide a valid email address.',
|
|
344
|
-
'password.min': 'Password must be at least 8 characters.',
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
}
|
|
232
|
+
# Rollback last batch
|
|
233
|
+
orchestr migrate:rollback
|
|
348
234
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
class UserController extends Controller {
|
|
352
|
-
@ValidateRequest() // Enables automatic validation
|
|
353
|
-
async store(request: StoreUserRequest, res: Response) {
|
|
354
|
-
// Request is already validated! Get safe data
|
|
355
|
-
const validated = request.validated();
|
|
235
|
+
# Rollback all migrations
|
|
236
|
+
orchestr migrate:reset
|
|
356
237
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
}
|
|
238
|
+
# Drop all tables and re-run migrations
|
|
239
|
+
orchestr migrate:fresh
|
|
361
240
|
|
|
362
|
-
|
|
241
|
+
# Rollback and re-run all migrations
|
|
242
|
+
orchestr migrate:refresh
|
|
363
243
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
const formRequest = await StoreUserRequest.validate(StoreUserRequest, req, res);
|
|
368
|
-
const validated = formRequest.validated();
|
|
369
|
-
const user = await User.create(validated);
|
|
370
|
-
return res.status(201).json({ user });
|
|
371
|
-
} catch (error) {
|
|
372
|
-
if (error instanceof ValidationException) return;
|
|
373
|
-
throw error;
|
|
374
|
-
}
|
|
375
|
-
});
|
|
244
|
+
# Check migration status
|
|
245
|
+
orchestr migrate:status
|
|
376
246
|
```
|
|
377
247
|
|
|
378
|
-
###
|
|
379
|
-
|
|
380
|
-
MVC pattern with base controller and dependency injection:
|
|
248
|
+
### Creating Tables
|
|
381
249
|
|
|
382
250
|
```typescript
|
|
383
|
-
import {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return res.json({ users });
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async show(req: Request, res: Response) {
|
|
399
|
-
const id = req.routeParam('id');
|
|
400
|
-
const user = await this.userService.findById(id);
|
|
401
|
-
return res.json({ user });
|
|
251
|
+
import { Migration, Schema } from '@orchestr-sh/orchestr';
|
|
252
|
+
|
|
253
|
+
export default class extends Migration {
|
|
254
|
+
async up(schema: Schema): Promise<void> {
|
|
255
|
+
await schema.create('users', (table) => {
|
|
256
|
+
table.id();
|
|
257
|
+
table.string('name');
|
|
258
|
+
table.string('email').unique();
|
|
259
|
+
table.string('password');
|
|
260
|
+
table.rememberToken();
|
|
261
|
+
table.timestamps();
|
|
262
|
+
});
|
|
402
263
|
}
|
|
403
264
|
|
|
404
|
-
async
|
|
405
|
-
|
|
406
|
-
name: 'required',
|
|
407
|
-
email: 'required|email',
|
|
408
|
-
});
|
|
409
|
-
const user = await this.userService.create(validated);
|
|
410
|
-
return res.status(201).json({ user });
|
|
265
|
+
async down(schema: Schema): Promise<void> {
|
|
266
|
+
await schema.dropIfExists('users');
|
|
411
267
|
}
|
|
412
268
|
}
|
|
413
|
-
|
|
414
|
-
// Register controller routes
|
|
415
|
-
Route.get('/users', [UserController, 'index']);
|
|
416
|
-
Route.get('/users/:id', [UserController, 'show']);
|
|
417
|
-
Route.post('/users', [UserController, 'store']);
|
|
418
269
|
```
|
|
419
270
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
### Request
|
|
423
|
-
|
|
424
|
-
Powerful request helper methods:
|
|
271
|
+
### Column Types
|
|
425
272
|
|
|
426
273
|
```typescript
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
// Route parameters
|
|
443
|
-
req.routeParam('id');
|
|
274
|
+
table.id() // Auto-incrementing big integer
|
|
275
|
+
table.string('name', 255) // VARCHAR
|
|
276
|
+
table.text('bio') // TEXT
|
|
277
|
+
table.integer('votes') // INTEGER
|
|
278
|
+
table.bigInteger('amount') // BIGINT
|
|
279
|
+
table.decimal('price', 8, 2) // DECIMAL
|
|
280
|
+
table.boolean('active') // BOOLEAN
|
|
281
|
+
table.date('birth_date') // DATE
|
|
282
|
+
table.datetime('published_at') // DATETIME
|
|
283
|
+
table.timestamp('created_at') // TIMESTAMP
|
|
284
|
+
table.timestamps() // created_at & updated_at
|
|
285
|
+
table.json('metadata') // JSON
|
|
286
|
+
table.uuid('identifier') // UUID
|
|
287
|
+
table.enum('status', ['draft', 'published'])
|
|
288
|
+
```
|
|
444
289
|
|
|
445
|
-
|
|
446
|
-
req.header('content-type');
|
|
447
|
-
req.expectsJson();
|
|
448
|
-
req.ajax();
|
|
290
|
+
### Column Modifiers
|
|
449
291
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
292
|
+
```typescript
|
|
293
|
+
table.string('email').nullable()
|
|
294
|
+
table.string('email').unique()
|
|
295
|
+
table.string('email').default('guest@example.com')
|
|
296
|
+
table.integer('votes').unsigned()
|
|
297
|
+
table.string('email').index()
|
|
454
298
|
```
|
|
455
299
|
|
|
456
|
-
###
|
|
457
|
-
|
|
458
|
-
Fluent response building:
|
|
300
|
+
### Foreign Keys
|
|
459
301
|
|
|
460
302
|
```typescript
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
303
|
+
table.bigInteger('user_id').unsigned();
|
|
304
|
+
table.foreign('user_id').references('id').on('users').onDelete('cascade');
|
|
305
|
+
```
|
|
464
306
|
|
|
465
|
-
|
|
466
|
-
res.header('X-Custom', 'value');
|
|
467
|
-
res.headers({ 'X-A': 'a', 'X-B': 'b' });
|
|
307
|
+
## Seeders
|
|
468
308
|
|
|
469
|
-
|
|
470
|
-
res.cookie('token', 'value', { httpOnly: true, maxAge: 3600 });
|
|
309
|
+
Populate your database with test or initial data.
|
|
471
310
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
311
|
+
```bash
|
|
312
|
+
# Create a seeder
|
|
313
|
+
orchestr make:seeder UserSeeder
|
|
475
314
|
|
|
476
|
-
|
|
477
|
-
|
|
315
|
+
# Run all seeders (runs DatabaseSeeder)
|
|
316
|
+
orchestr db:seed
|
|
478
317
|
|
|
479
|
-
|
|
480
|
-
|
|
318
|
+
# Run a specific seeder
|
|
319
|
+
orchestr db:seed --class=UserSeeder
|
|
481
320
|
```
|
|
482
321
|
|
|
483
|
-
###
|
|
484
|
-
|
|
485
|
-
Static access to services:
|
|
322
|
+
### Creating Seeders
|
|
486
323
|
|
|
487
324
|
```typescript
|
|
488
|
-
import {
|
|
325
|
+
import { Seeder } from '@orchestr-sh/orchestr';
|
|
489
326
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const users = await DB.table('users').where('active', true).get();
|
|
327
|
+
export default class UserSeeder extends Seeder {
|
|
328
|
+
async run(): Promise<void> {
|
|
329
|
+
await this.connection?.table('users').insert([
|
|
330
|
+
{ name: 'John Doe', email: 'john@example.com' },
|
|
331
|
+
{ name: 'Jane Smith', email: 'jane@example.com' },
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
499
335
|
```
|
|
500
336
|
|
|
501
|
-
###
|
|
502
|
-
|
|
503
|
-
Fluent, chainable query builder with full Laravel API:
|
|
337
|
+
### DatabaseSeeder Pattern
|
|
504
338
|
|
|
505
339
|
```typescript
|
|
506
|
-
import {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const users = await DB.table('users').get();
|
|
510
|
-
const user = await DB.table('users').where('id', 1).first();
|
|
511
|
-
|
|
512
|
-
// Where clauses
|
|
513
|
-
await DB.table('users')
|
|
514
|
-
.where('votes', '>', 100)
|
|
515
|
-
.where('status', 'active')
|
|
516
|
-
.get();
|
|
517
|
-
|
|
518
|
-
// Or where
|
|
519
|
-
await DB.table('users')
|
|
520
|
-
.where('votes', '>', 100)
|
|
521
|
-
.orWhere('name', 'John')
|
|
522
|
-
.get();
|
|
523
|
-
|
|
524
|
-
// Additional where methods
|
|
525
|
-
await DB.table('users').whereBetween('votes', [1, 100]).get();
|
|
526
|
-
await DB.table('users').whereIn('id', [1, 2, 3]).get();
|
|
527
|
-
await DB.table('users').whereNull('deleted_at').get();
|
|
528
|
-
|
|
529
|
-
// Ordering, grouping, and limits
|
|
530
|
-
await DB.table('users')
|
|
531
|
-
.orderBy('name', 'desc')
|
|
532
|
-
.groupBy('account_id')
|
|
533
|
-
.having('account_id', '>', 100)
|
|
534
|
-
.limit(10)
|
|
535
|
-
.offset(20)
|
|
536
|
-
.get();
|
|
537
|
-
|
|
538
|
-
// Joins
|
|
539
|
-
await DB.table('users')
|
|
540
|
-
.join('contacts', 'users.id', '=', 'contacts.user_id')
|
|
541
|
-
.leftJoin('orders', 'users.id', '=', 'orders.user_id')
|
|
542
|
-
.select('users.*', 'contacts.phone', 'orders.price')
|
|
543
|
-
.get();
|
|
340
|
+
import { Seeder } from '@orchestr-sh/orchestr';
|
|
341
|
+
import UserSeeder from './UserSeeder';
|
|
342
|
+
import PostSeeder from './PostSeeder';
|
|
544
343
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const avg = await DB.table('orders').avg('price');
|
|
550
|
-
const sum = await DB.table('orders').sum('price');
|
|
551
|
-
|
|
552
|
-
// Inserts
|
|
553
|
-
await DB.table('users').insert({
|
|
554
|
-
name: 'John',
|
|
555
|
-
email: 'john@example.com'
|
|
556
|
-
});
|
|
344
|
+
export default class DatabaseSeeder extends Seeder {
|
|
345
|
+
async run(): Promise<void> {
|
|
346
|
+
await this.call(UserSeeder);
|
|
347
|
+
await this.call(PostSeeder);
|
|
557
348
|
|
|
558
|
-
//
|
|
559
|
-
await
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
// Deletes
|
|
564
|
-
await DB.table('users').where('votes', '<', 100).delete();
|
|
565
|
-
|
|
566
|
-
// Raw expressions
|
|
567
|
-
await DB.table('users')
|
|
568
|
-
.select(DB.raw('count(*) as user_count, status'))
|
|
569
|
-
.where('status', '<>', 1)
|
|
570
|
-
.groupBy('status')
|
|
571
|
-
.get();
|
|
349
|
+
// Or call multiple seeders at once
|
|
350
|
+
await this.callMany([UserSeeder, PostSeeder]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
572
353
|
```
|
|
573
354
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
ActiveRecord ORM (Eloquent equivalent) with relationships and advanced features:
|
|
355
|
+
## Controllers
|
|
577
356
|
|
|
578
357
|
```typescript
|
|
579
|
-
import {
|
|
358
|
+
import { Controller, Injectable, ValidateRequest } from '@orchestr-sh/orchestr';
|
|
580
359
|
|
|
581
|
-
|
|
582
|
-
class
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
protected hidden = ['password'];
|
|
586
|
-
protected casts = {
|
|
587
|
-
email_verified_at: 'datetime',
|
|
588
|
-
is_admin: 'boolean'
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
// One-to-One: User has one profile
|
|
592
|
-
profile(): HasOne<Profile, User> {
|
|
593
|
-
return this.hasOne(Profile);
|
|
360
|
+
@Injectable()
|
|
361
|
+
export class UserController extends Controller {
|
|
362
|
+
constructor(private service: UserService) {
|
|
363
|
+
super();
|
|
594
364
|
}
|
|
595
365
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
366
|
+
@ValidateRequest()
|
|
367
|
+
async index(req: GetUsersRequest, res: any) {
|
|
368
|
+
const users = await User.query().with('posts').get();
|
|
369
|
+
return res.json({ data: users });
|
|
599
370
|
}
|
|
600
371
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
return
|
|
604
|
-
|
|
605
|
-
.withTimestamps();
|
|
372
|
+
async show(req: any, res: any) {
|
|
373
|
+
const user = await User.find(req.routeParam('id'));
|
|
374
|
+
if (!user) return res.status(404).json({ error: 'Not found' });
|
|
375
|
+
return res.json({ data: user });
|
|
606
376
|
}
|
|
607
377
|
}
|
|
608
378
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
user(): BelongsTo<User, Profile> {
|
|
614
|
-
return this.belongsTo(User);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
379
|
+
// Register route
|
|
380
|
+
Route.get('/users', [UserController, 'index']);
|
|
381
|
+
Route.get('/users/:id', [UserController, 'show']);
|
|
382
|
+
```
|
|
617
383
|
|
|
618
|
-
|
|
619
|
-
protected table = 'posts';
|
|
384
|
+
## FormRequest Validation
|
|
620
385
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return this.belongsTo(User, 'user_id');
|
|
624
|
-
}
|
|
386
|
+
```typescript
|
|
387
|
+
import { FormRequest, ValidationRules } from '@orchestr-sh/orchestr';
|
|
625
388
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return
|
|
389
|
+
export class StoreUserRequest extends FormRequest {
|
|
390
|
+
protected authorize(): boolean {
|
|
391
|
+
return true; // Add authorization logic
|
|
629
392
|
}
|
|
630
393
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
394
|
+
protected rules(): ValidationRules {
|
|
395
|
+
return {
|
|
396
|
+
name: 'required|string|min:3',
|
|
397
|
+
email: 'required|email',
|
|
398
|
+
password: 'required|min:8',
|
|
399
|
+
};
|
|
634
400
|
}
|
|
635
401
|
}
|
|
636
402
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
403
|
+
// Use with @ValidateRequest decorator
|
|
404
|
+
@Injectable()
|
|
405
|
+
export class UserController extends Controller {
|
|
406
|
+
@ValidateRequest()
|
|
407
|
+
async store(req: StoreUserRequest, res: any) {
|
|
408
|
+
const validated = req.validated();
|
|
409
|
+
const user = await User.create(validated);
|
|
410
|
+
return res.status(201).json({ data: user });
|
|
643
411
|
}
|
|
644
412
|
}
|
|
413
|
+
```
|
|
645
414
|
|
|
646
|
-
|
|
647
|
-
const users = await User.query().where('active', true).get();
|
|
648
|
-
const user = await User.query().find(1);
|
|
649
|
-
|
|
650
|
-
// Lazy loading relationships
|
|
651
|
-
await user.load('posts');
|
|
652
|
-
await user.load(['posts', 'profile']);
|
|
653
|
-
const posts = user.getRelation('posts');
|
|
654
|
-
|
|
655
|
-
// Eager loading (solves N+1 problem)
|
|
656
|
-
const users = await User.query()
|
|
657
|
-
.with(['posts.comments', 'profile'])
|
|
658
|
-
.get();
|
|
659
|
-
|
|
660
|
-
// Eager load with constraints
|
|
661
|
-
const users = await User.query()
|
|
662
|
-
.with({
|
|
663
|
-
posts: (query) => query.where('published', '=', true)
|
|
664
|
-
})
|
|
665
|
-
.get();
|
|
666
|
-
|
|
667
|
-
// Create related models
|
|
668
|
-
const post = await user.posts().create({
|
|
669
|
-
title: 'My Post',
|
|
670
|
-
content: 'Content here'
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
// Associate/dissociate (BelongsTo)
|
|
674
|
-
const post = new Post();
|
|
675
|
-
post.author().associate(user);
|
|
676
|
-
await post.save();
|
|
677
|
-
|
|
678
|
-
// Many-to-Many operations
|
|
679
|
-
const user = await User.find(1);
|
|
680
|
-
|
|
681
|
-
// Attach roles
|
|
682
|
-
await user.roles().attach([1, 2, 3]);
|
|
683
|
-
await user.roles().attach(1, {
|
|
684
|
-
expires_at: new Date('2025-12-31'),
|
|
685
|
-
granted_by: 'admin'
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
// Detach roles
|
|
689
|
-
await user.roles().detach([1, 2]);
|
|
690
|
-
await user.roles().detach(); // detach all
|
|
691
|
-
|
|
692
|
-
// Sync roles (detach all existing, attach new)
|
|
693
|
-
const changes = await user.roles().sync([1, 2, 3]);
|
|
694
|
-
|
|
695
|
-
// Toggle roles (attach if not attached, detach if attached)
|
|
696
|
-
await user.roles().toggle([1, 2, 3]);
|
|
697
|
-
|
|
698
|
-
// Query with pivot constraints
|
|
699
|
-
const activeRoles = await user.roles()
|
|
700
|
-
.wherePivot('expires_at', '>', new Date())
|
|
701
|
-
.get();
|
|
702
|
-
|
|
703
|
-
// Update pivot data
|
|
704
|
-
await user.roles().updateExistingPivot(1, {
|
|
705
|
-
expires_at: new Date('2027-12-31')
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
// Create
|
|
709
|
-
const user = new User();
|
|
710
|
-
user.name = 'John Doe';
|
|
711
|
-
user.email = 'john@example.com';
|
|
712
|
-
await user.save();
|
|
415
|
+
## Configuration
|
|
713
416
|
|
|
714
|
-
|
|
715
|
-
const user = await User.query().create({
|
|
716
|
-
name: 'John Doe',
|
|
717
|
-
email: 'john@example.com'
|
|
718
|
-
});
|
|
417
|
+
### Database Setup
|
|
719
418
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
user.name = 'Jane Doe';
|
|
723
|
-
await user.save();
|
|
419
|
+
```typescript
|
|
420
|
+
import { DatabaseServiceProvider, DatabaseManager, DrizzleAdapter } from '@orchestr-sh/orchestr';
|
|
724
421
|
|
|
725
|
-
|
|
726
|
-
|
|
422
|
+
export class DatabaseServiceProvider extends ServiceProvider {
|
|
423
|
+
register(): void {
|
|
424
|
+
this.app.singleton('db', () => {
|
|
425
|
+
const config = this.app.make('config').get('database');
|
|
426
|
+
const manager = new DatabaseManager(config);
|
|
427
|
+
manager.registerAdapter('drizzle', (config) => new DrizzleAdapter(config));
|
|
428
|
+
return manager;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
727
431
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
432
|
+
async boot(): Promise<void> {
|
|
433
|
+
const db = this.app.make('db');
|
|
434
|
+
await db.connection().connect();
|
|
435
|
+
Ensemble.setConnectionResolver(db);
|
|
436
|
+
}
|
|
731
437
|
}
|
|
732
438
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
await article.forceDelete(); // Permanent delete
|
|
737
|
-
|
|
738
|
-
// Query only non-deleted
|
|
739
|
-
const articles = await Article.query().get();
|
|
740
|
-
|
|
741
|
-
// Query with trashed
|
|
742
|
-
const allArticles = await Article.query().withTrashed().get();
|
|
439
|
+
// Register in your app
|
|
440
|
+
app.register(DatabaseServiceProvider);
|
|
441
|
+
```
|
|
743
442
|
|
|
744
|
-
|
|
745
|
-
const trashedArticles = await Article.query().onlyTrashed().get();
|
|
443
|
+
### Service Providers
|
|
746
444
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
class Post extends Ensemble {
|
|
750
|
-
protected table = 'posts';
|
|
751
|
-
public timestamps = true; // enabled by default
|
|
752
|
-
}
|
|
445
|
+
```typescript
|
|
446
|
+
import { ServiceProvider } from '@orchestr-sh/orchestr';
|
|
753
447
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
email_verified_at: 'datetime',
|
|
758
|
-
settings: 'json',
|
|
759
|
-
is_admin: 'boolean',
|
|
760
|
-
age: 'number'
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
// Accessors
|
|
764
|
-
getFullNameAttribute(): string {
|
|
765
|
-
return `${this.getAttribute('first_name')} ${this.getAttribute('last_name')}`;
|
|
448
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
449
|
+
register(): void {
|
|
450
|
+
this.app.singleton('myService', () => new MyService());
|
|
766
451
|
}
|
|
767
452
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
this.setAttribute('password', hashPassword(value));
|
|
453
|
+
async boot(): Promise<void> {
|
|
454
|
+
// Bootstrap code
|
|
771
455
|
}
|
|
772
456
|
}
|
|
773
|
-
|
|
774
|
-
const user = await User.query().find(1);
|
|
775
|
-
console.log(user.full_name); // Uses accessor
|
|
776
|
-
user.password = 'secret123'; // Uses mutator
|
|
777
457
|
```
|
|
778
458
|
|
|
779
|
-
|
|
459
|
+
## API Reference
|
|
780
460
|
|
|
781
|
-
|
|
461
|
+
### Ensemble Methods
|
|
782
462
|
|
|
783
463
|
```typescript
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
//
|
|
796
|
-
|
|
797
|
-
await user.roles().attach(1, { expires_at: new Date('2025-12-31') });
|
|
798
|
-
|
|
799
|
-
// Detach roles
|
|
800
|
-
await user.roles().detach([1, 2]);
|
|
801
|
-
|
|
802
|
-
// Sync (detach all, attach new)
|
|
803
|
-
const { attached, detached, updated } = await user.roles().sync([1, 2, 3]);
|
|
804
|
-
|
|
805
|
-
// Toggle (attach if not present, detach if present)
|
|
806
|
-
await user.roles().toggle([1, 2]);
|
|
807
|
-
|
|
808
|
-
// Query with pivot constraints
|
|
809
|
-
const activeRoles = await user.roles()
|
|
810
|
-
.wherePivot('expires_at', '>', new Date())
|
|
811
|
-
.wherePivotNotNull('granted_by')
|
|
812
|
-
.get();
|
|
813
|
-
|
|
814
|
-
// Update pivot data
|
|
815
|
-
await user.roles().updateExistingPivot(1, {
|
|
816
|
-
expires_at: new Date('2027-12-31')
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
// Eager load with pivot data
|
|
820
|
-
const users = await User.query()
|
|
821
|
-
.with({
|
|
822
|
-
roles: (query) => query.wherePivot('active', true)
|
|
823
|
-
})
|
|
824
|
-
.get();
|
|
825
|
-
|
|
826
|
-
// Access pivot data
|
|
827
|
-
const roles = await user.roles().get();
|
|
828
|
-
const pivot = roles[0].getRelation('pivot');
|
|
829
|
-
// { user_id, role_id, expires_at, granted_by, created_at, updated_at }
|
|
464
|
+
// Query
|
|
465
|
+
User.query() // Get query builder
|
|
466
|
+
User.find(id) // Find by primary key
|
|
467
|
+
User.findOrFail(id) // Find or throw error
|
|
468
|
+
User.all() // Get all records
|
|
469
|
+
User.create(data) // Create and save
|
|
470
|
+
|
|
471
|
+
// Instance methods
|
|
472
|
+
user.save() // Save changes
|
|
473
|
+
user.delete() // Delete record
|
|
474
|
+
user.refresh() // Reload from database
|
|
475
|
+
user.load('posts') // Lazy load relationship
|
|
476
|
+
user.toObject() // Convert to plain object
|
|
830
477
|
```
|
|
831
478
|
|
|
832
|
-
|
|
833
|
-
- ✅ Automatic pivot table naming (alphabetically sorted model names)
|
|
834
|
-
- ✅ Attach/detach/sync/toggle operations
|
|
835
|
-
- ✅ Pivot table queries (wherePivot, wherePivotIn, wherePivotNull, etc.)
|
|
836
|
-
- ✅ Additional pivot columns with `withPivot()`
|
|
837
|
-
- ✅ Automatic pivot timestamps with `withTimestamps()`
|
|
838
|
-
- ✅ Custom pivot accessor with `as()`
|
|
839
|
-
- ✅ Update pivot data with `updateExistingPivot()`
|
|
840
|
-
- ✅ Full eager loading support
|
|
841
|
-
|
|
842
|
-
### Database Setup
|
|
843
|
-
|
|
844
|
-
Configure multiple database connections:
|
|
479
|
+
### Query Builder Methods
|
|
845
480
|
|
|
846
481
|
```typescript
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
}));
|
|
867
|
-
|
|
868
|
-
await app.boot();
|
|
869
|
-
|
|
870
|
-
// Use default connection
|
|
871
|
-
const users = await DB.table('users').get();
|
|
872
|
-
|
|
873
|
-
// Use specific connection
|
|
874
|
-
const posts = await DB.connection('postgres').table('posts').get();
|
|
482
|
+
.where(column, value)
|
|
483
|
+
.where(column, operator, value)
|
|
484
|
+
.orWhere(column, value)
|
|
485
|
+
.whereIn(column, array)
|
|
486
|
+
.whereBetween(column, [min, max])
|
|
487
|
+
.whereNull(column)
|
|
488
|
+
.orderBy(column, direction)
|
|
489
|
+
.limit(number)
|
|
490
|
+
.offset(number)
|
|
491
|
+
.join(table, first, operator, second)
|
|
492
|
+
.groupBy(column)
|
|
493
|
+
.having(column, operator, value)
|
|
494
|
+
.select(columns)
|
|
495
|
+
.count()
|
|
496
|
+
.sum(column)
|
|
497
|
+
.avg(column)
|
|
498
|
+
.min(column)
|
|
499
|
+
.max(column)
|
|
875
500
|
```
|
|
876
501
|
|
|
877
|
-
|
|
502
|
+
### Schema Methods
|
|
878
503
|
|
|
879
|
-
Here's a complete example showing routing, database, and ORM:
|
|
880
|
-
|
|
881
|
-
**index.ts**
|
|
882
504
|
```typescript
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
// Register database
|
|
892
|
-
app.register(new DatabaseServiceProvider({
|
|
893
|
-
default: 'sqlite',
|
|
894
|
-
connections: {
|
|
895
|
-
sqlite: {
|
|
896
|
-
adapter: 'drizzle',
|
|
897
|
-
client: drizzle(new Database('database.sqlite'))
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}));
|
|
901
|
-
|
|
902
|
-
// Register routes
|
|
903
|
-
app.register(AppRouteServiceProvider);
|
|
904
|
-
|
|
905
|
-
await app.boot();
|
|
906
|
-
|
|
907
|
-
const kernel = new Kernel(app);
|
|
908
|
-
kernel.listen(3000);
|
|
505
|
+
schema.create(table, callback) // Create new table
|
|
506
|
+
schema.table(table, callback) // Modify existing table
|
|
507
|
+
schema.drop(table) // Drop table
|
|
508
|
+
schema.dropIfExists(table) // Drop table if exists
|
|
509
|
+
schema.rename(from, to) // Rename table
|
|
510
|
+
schema.hasTable(table) // Check if table exists
|
|
511
|
+
schema.hasColumn(table, column) // Check if column exists
|
|
909
512
|
```
|
|
910
513
|
|
|
911
|
-
|
|
912
|
-
```typescript
|
|
913
|
-
import { Ensemble, HasMany, softDeletes } from 'orchestr';
|
|
914
|
-
import { Post } from './Post';
|
|
915
|
-
|
|
916
|
-
export class User extends softDeletes(Ensemble) {
|
|
917
|
-
protected table = 'users';
|
|
918
|
-
protected fillable = ['name', 'email', 'password'];
|
|
919
|
-
protected hidden = ['password'];
|
|
920
|
-
|
|
921
|
-
protected casts = {
|
|
922
|
-
email_verified_at: 'datetime',
|
|
923
|
-
is_admin: 'boolean'
|
|
924
|
-
};
|
|
514
|
+
### Blueprint Column Types
|
|
925
515
|
|
|
926
|
-
// Define relationship
|
|
927
|
-
posts(): HasMany<Post, User> {
|
|
928
|
-
return this.hasMany(Post);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
**app/Models/Post.ts**
|
|
934
516
|
```typescript
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
517
|
+
table.id() // Auto-increment big integer
|
|
518
|
+
table.increments(column) // Auto-increment integer
|
|
519
|
+
table.bigIncrements(column) // Auto-increment big integer
|
|
520
|
+
table.string(column, length) // VARCHAR
|
|
521
|
+
table.text(column) // TEXT
|
|
522
|
+
table.mediumText(column) // MEDIUMTEXT
|
|
523
|
+
table.longText(column) // LONGTEXT
|
|
524
|
+
table.integer(column) // INTEGER
|
|
525
|
+
table.bigInteger(column) // BIGINT
|
|
526
|
+
table.smallInteger(column) // SMALLINT
|
|
527
|
+
table.tinyInteger(column) // TINYINT
|
|
528
|
+
table.decimal(column, precision, scale)
|
|
529
|
+
table.float(column, precision, scale)
|
|
530
|
+
table.double(column, precision, scale)
|
|
531
|
+
table.boolean(column) // BOOLEAN
|
|
532
|
+
table.date(column) // DATE
|
|
533
|
+
table.datetime(column, precision) // DATETIME
|
|
534
|
+
table.timestamp(column, precision) // TIMESTAMP
|
|
535
|
+
table.timestamps(precision) // created_at & updated_at
|
|
536
|
+
table.json(column) // JSON
|
|
537
|
+
table.jsonb(column) // JSONB
|
|
538
|
+
table.uuid(column) // UUID
|
|
539
|
+
table.enum(column, values) // ENUM
|
|
540
|
+
table.binary(column) // BINARY
|
|
541
|
+
table.rememberToken() // remember_token VARCHAR(100)
|
|
542
|
+
table.softDeletes(column) // deleted_at timestamp
|
|
946
543
|
```
|
|
947
544
|
|
|
948
|
-
|
|
545
|
+
### Relationship Methods
|
|
546
|
+
|
|
949
547
|
```typescript
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
.find(req.routeParam('id'));
|
|
969
|
-
|
|
970
|
-
if (!user) {
|
|
971
|
-
return res.status(404).json({ message: 'User not found' });
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
return res.json({ user: user.toObject() });
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
Route.post('/users', async (req, res) => {
|
|
978
|
-
const user = await User.query().create(
|
|
979
|
-
req.only(['name', 'email', 'password'])
|
|
980
|
-
);
|
|
981
|
-
|
|
982
|
-
return res.status(201).json({ user });
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
Route.delete('/users/:id', async (req, res) => {
|
|
986
|
-
const user = await User.query().find(req.routeParam('id'));
|
|
987
|
-
await user?.delete(); // Soft delete
|
|
988
|
-
|
|
989
|
-
return res.json({ message: 'User deleted' });
|
|
990
|
-
});
|
|
991
|
-
});
|
|
548
|
+
// HasOne, HasMany, BelongsTo
|
|
549
|
+
.get() // Execute query
|
|
550
|
+
.first() // Get first result
|
|
551
|
+
.create(data) // Create related model
|
|
552
|
+
.where(column, value) // Add constraint
|
|
553
|
+
|
|
554
|
+
// BelongsToMany
|
|
555
|
+
.attach(ids) // Attach related models
|
|
556
|
+
.detach(ids) // Detach related models
|
|
557
|
+
.sync(ids) // Sync relationships
|
|
558
|
+
.toggle(ids) // Toggle relationships
|
|
559
|
+
.wherePivot(column, value) // Query pivot table
|
|
560
|
+
.updateExistingPivot(id, data) // Update pivot data
|
|
561
|
+
|
|
562
|
+
// All relationships
|
|
563
|
+
.with('relation') // Eager load
|
|
564
|
+
.with(['relation1', 'relation2'])
|
|
565
|
+
.with({ relation: (q) => q.where(...) })
|
|
992
566
|
```
|
|
993
567
|
|
|
994
|
-
|
|
568
|
+
### Available Relationships
|
|
995
569
|
|
|
996
|
-
|
|
570
|
+
- `HasOne` - One-to-one
|
|
571
|
+
- `HasMany` - One-to-many
|
|
572
|
+
- `BelongsTo` - Inverse of HasOne/HasMany
|
|
573
|
+
- `BelongsToMany` - Many-to-many
|
|
574
|
+
- `MorphOne` - Polymorphic one-to-one
|
|
575
|
+
- `MorphMany` - Polymorphic one-to-many
|
|
576
|
+
- `MorphTo` - Inverse of MorphOne/MorphMany
|
|
577
|
+
- `MorphToMany` - Polymorphic many-to-many
|
|
578
|
+
- `MorphedByMany` - Inverse of MorphToMany
|
|
997
579
|
|
|
580
|
+
### CLI Commands
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
# Migrations
|
|
584
|
+
orchestr make:migration <name> # Create migration
|
|
585
|
+
orchestr make:migration <name> --create=<table> # Create table migration
|
|
586
|
+
orchestr make:migration <name> --table=<table> # Update table migration
|
|
587
|
+
orchestr migrate # Run migrations
|
|
588
|
+
orchestr migrate:rollback # Rollback last batch
|
|
589
|
+
orchestr migrate:reset # Rollback all migrations
|
|
590
|
+
orchestr migrate:refresh # Reset and re-run migrations
|
|
591
|
+
orchestr migrate:fresh # Drop all tables and migrate
|
|
592
|
+
orchestr migrate:status # Show migration status
|
|
593
|
+
|
|
594
|
+
# Seeders
|
|
595
|
+
orchestr make:seeder <name> # Create seeder
|
|
596
|
+
orchestr db:seed # Run DatabaseSeeder
|
|
597
|
+
orchestr db:seed --class=<name> # Run specific seeder
|
|
998
598
|
```
|
|
999
|
-
src/
|
|
1000
|
-
├── Container/
|
|
1001
|
-
│ └── Container.ts # IoC Container with DI
|
|
1002
|
-
├── Foundation/
|
|
1003
|
-
│ ├── Application.ts # Core application class
|
|
1004
|
-
│ ├── ServiceProvider.ts # Service provider base
|
|
1005
|
-
│ └── Http/
|
|
1006
|
-
│ └── Kernel.ts # HTTP kernel
|
|
1007
|
-
├── Routing/
|
|
1008
|
-
│ ├── Router.ts # Route registration and dispatch
|
|
1009
|
-
│ ├── Route.ts # Individual route
|
|
1010
|
-
│ ├── Request.ts # HTTP request wrapper
|
|
1011
|
-
│ ├── Response.ts # HTTP response wrapper
|
|
1012
|
-
│ └── Controller.ts # Base controller
|
|
1013
|
-
├── Database/
|
|
1014
|
-
│ ├── DatabaseManager.ts # Multi-connection manager
|
|
1015
|
-
│ ├── Connection.ts # Database connection
|
|
1016
|
-
│ ├── Query/
|
|
1017
|
-
│ │ ├── Builder.ts # Query builder
|
|
1018
|
-
│ │ └── Expression.ts # Raw SQL expressions
|
|
1019
|
-
│ ├── Ensemble/
|
|
1020
|
-
│ │ ├── Ensemble.ts # Base ORM model (like Eloquent)
|
|
1021
|
-
│ │ ├── EnsembleBuilder.ts # Model query builder
|
|
1022
|
-
│ │ ├── EnsembleCollection.ts # Model collection
|
|
1023
|
-
│ │ ├── SoftDeletes.ts # Soft delete trait
|
|
1024
|
-
│ │ ├── Relations/
|
|
1025
|
-
│ │ │ ├── Relation.ts # Base relation class
|
|
1026
|
-
│ │ │ ├── HasOne.ts # One-to-one relationship
|
|
1027
|
-
│ │ │ ├── HasMany.ts # One-to-many relationship
|
|
1028
|
-
│ │ │ ├── BelongsTo.ts # Inverse relationship
|
|
1029
|
-
│ │ │ └── BelongsToMany.ts # Many-to-many relationship
|
|
1030
|
-
│ │ └── Concerns/
|
|
1031
|
-
│ │ ├── HasAttributes.ts # Attribute handling & casting
|
|
1032
|
-
│ │ ├── HasTimestamps.ts # Timestamp management
|
|
1033
|
-
│ │ └── HasRelationships.ts # Relationship functionality
|
|
1034
|
-
│ ├── Adapters/
|
|
1035
|
-
│ │ └── DrizzleAdapter.ts # Drizzle ORM adapter
|
|
1036
|
-
│ └── DatabaseServiceProvider.ts
|
|
1037
|
-
├── Support/
|
|
1038
|
-
│ ├── Facade.ts # Facade base class
|
|
1039
|
-
│ └── helpers.ts # Helper functions
|
|
1040
|
-
├── Facades/
|
|
1041
|
-
│ ├── Route.ts # Route facade
|
|
1042
|
-
│ └── DB.ts # Database facade
|
|
1043
|
-
└── Providers/
|
|
1044
|
-
└── RouteServiceProvider.ts # Route service provider
|
|
1045
|
-
```
|
|
1046
599
|
|
|
1047
|
-
##
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
-
|
|
1052
|
-
-
|
|
1053
|
-
-
|
|
1054
|
-
-
|
|
1055
|
-
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
-
|
|
1062
|
-
-
|
|
1063
|
-
-
|
|
1064
|
-
- [x] HTTP Router & Route Files
|
|
1065
|
-
- [x] Request/Response
|
|
1066
|
-
- [x] Middleware Pipeline
|
|
1067
|
-
- [x] Controllers
|
|
1068
|
-
- [x] Facades (Route, DB)
|
|
1069
|
-
- [x] Database Query Builder
|
|
1070
|
-
- [x] Ensemble ORM (Eloquent equivalent)
|
|
1071
|
-
- [x] Multi-connection Database Manager
|
|
1072
|
-
- [x] Soft Deletes
|
|
1073
|
-
- [x] Model Attributes & Casting
|
|
1074
|
-
- [x] Model Relationships (HasOne, HasMany, BelongsTo)
|
|
1075
|
-
- [x] Many-to-Many Relationships (BelongsToMany)
|
|
1076
|
-
- [x] Eager/Lazy Loading
|
|
1077
|
-
- [x] FormRequest Validation & Authorization
|
|
1078
|
-
- [ ] Relationship Queries (has, whereHas, withCount)
|
|
1079
|
-
- [ ] Polymorphic Relationships
|
|
1080
|
-
- [ ] Database Migrations
|
|
1081
|
-
- [ ] Database Seeding
|
|
1082
|
-
- [ ] Authentication & Authorization
|
|
1083
|
-
- [ ] Queue System
|
|
1084
|
-
- [ ] Events & Listeners
|
|
1085
|
-
- [ ] File Storage
|
|
1086
|
-
- [ ] Cache System
|
|
1087
|
-
- [ ] Template Engine (Blade equivalent)
|
|
1088
|
-
- [ ] CLI/Artisan equivalent
|
|
1089
|
-
- [ ] Testing utilities
|
|
1090
|
-
|
|
1091
|
-
## Comparison to Laravel
|
|
1092
|
-
|
|
1093
|
-
| Feature | Laravel | Orchestr |
|
|
1094
|
-
|---------|---------|----------|
|
|
1095
|
-
| Service Container | ✅ | ✅ |
|
|
1096
|
-
| Service Providers | ✅ | ✅ |
|
|
1097
|
-
| Configuration | ✅ | ✅ |
|
|
1098
|
-
| Routing | ✅ | ✅ |
|
|
1099
|
-
| Route Files | ✅ | ✅ |
|
|
1100
|
-
| Middleware | ✅ | ✅ |
|
|
1101
|
-
| Controllers | ✅ | ✅ |
|
|
1102
|
-
| Request/Response | ✅ | ✅ |
|
|
1103
|
-
| Facades | ✅ | ✅ |
|
|
1104
|
-
| Query Builder | ✅ | ✅ |
|
|
1105
|
-
| Eloquent ORM | ✅ | ✅ (Ensemble) |
|
|
1106
|
-
| Soft Deletes | ✅ | ✅ |
|
|
1107
|
-
| Timestamps | ✅ | ✅ |
|
|
1108
|
-
| Attribute Casting | ✅ | ✅ |
|
|
1109
|
-
| Basic Relationships | ✅ | ✅ |
|
|
1110
|
-
| Eager/Lazy Loading | ✅ | ✅ |
|
|
1111
|
-
| Many-to-Many | ✅ | ✅ |
|
|
1112
|
-
| Polymorphic Relations | ✅ | 🚧 |
|
|
1113
|
-
| Migrations | ✅ | 🚧 |
|
|
1114
|
-
| Seeding | ✅ | 🚧 |
|
|
1115
|
-
| FormRequest Validation | ✅ | ✅ |
|
|
1116
|
-
| Authentication | ✅ | 🚧 |
|
|
1117
|
-
| Authorization | ✅ | 🚧 |
|
|
1118
|
-
| Events | ✅ | 🚧 |
|
|
1119
|
-
| Queues | ✅ | 🚧 |
|
|
1120
|
-
| Cache | ✅ | 🚧 |
|
|
1121
|
-
| File Storage | ✅ | 🚧 |
|
|
1122
|
-
| Mail | ✅ | 🚧 |
|
|
1123
|
-
| Notifications | ✅ | 🚧 |
|
|
600
|
+
## Features
|
|
601
|
+
|
|
602
|
+
- ✅ Service Container & Dependency Injection
|
|
603
|
+
- ✅ Configuration System
|
|
604
|
+
- ✅ HTTP Router & Middleware
|
|
605
|
+
- ✅ Controllers with DI
|
|
606
|
+
- ✅ FormRequest Validation
|
|
607
|
+
- ✅ Query Builder
|
|
608
|
+
- ✅ Ensemble ORM (ActiveRecord)
|
|
609
|
+
- ✅ Relationships (Standard + Polymorphic)
|
|
610
|
+
- ✅ Eager/Lazy Loading
|
|
611
|
+
- ✅ Migrations with Schema Builder
|
|
612
|
+
- ✅ Database Seeders
|
|
613
|
+
- ✅ Soft Deletes
|
|
614
|
+
- ✅ Attribute Casting
|
|
615
|
+
- ✅ Timestamps
|
|
616
|
+
- ✅ @DynamicRelation Decorator
|
|
1124
617
|
|
|
1125
618
|
## License
|
|
1126
619
|
|
|
1127
620
|
MIT
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
Built with TypeScript. Inspired by Laravel.
|