@orchestr-sh/orchestr 1.5.9 → 1.5.11
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 +275 -977
- package/dist/Database/Ensemble/Concerns/HasDynamicRelationGetters.d.ts +27 -0
- package/dist/Database/Ensemble/Concerns/HasDynamicRelationGetters.d.ts.map +1 -0
- package/dist/Database/Ensemble/Concerns/HasDynamicRelationGetters.js +109 -0
- package/dist/Database/Ensemble/Concerns/HasDynamicRelationGetters.js.map +1 -0
- package/dist/Database/Ensemble/Concerns/HasDynamicRelations.d.ts +59 -3
- package/dist/Database/Ensemble/Concerns/HasDynamicRelations.d.ts.map +1 -1
- package/dist/Database/Ensemble/Concerns/HasDynamicRelations.js +159 -31
- 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/Ensemble.d.ts.map +1 -1
- package/dist/Database/Ensemble/Ensemble.js +0 -4
- package/dist/Database/Ensemble/Ensemble.js.map +1 -1
- package/dist/Database/Ensemble/EnsembleBuilder.d.ts +1 -1
- package/dist/Database/Ensemble/EnsembleBuilder.d.ts.map +1 -1
- package/dist/Database/Ensemble/EnsembleBuilder.js +7 -10
- package/dist/Database/Ensemble/EnsembleBuilder.js.map +1 -1
- package/dist/Database/Ensemble/Relations/BelongsTo.d.ts +5 -0
- package/dist/Database/Ensemble/Relations/BelongsTo.d.ts.map +1 -1
- package/dist/Database/Ensemble/Relations/BelongsTo.js +8 -0
- package/dist/Database/Ensemble/Relations/BelongsTo.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/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,1127 +1,425 @@
|
|
|
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 {
|
|
74
|
-
|
|
75
|
-
const app = new Application(__dirname);
|
|
51
|
+
import { Ensemble } from '@orchestr-sh/orchestr';
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 {
|
|
166
|
+
import { Ensemble, MorphMany, MorphTo, DynamicRelation } from '@orchestr-sh/orchestr';
|
|
264
167
|
|
|
265
|
-
export class
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.
|
|
269
|
-
|
|
270
|
-
// Load API routes
|
|
271
|
-
this.routes(() => import('../../routes/api'));
|
|
272
|
-
|
|
273
|
-
await super.boot();
|
|
168
|
+
export class Post extends Ensemble {
|
|
169
|
+
@DynamicRelation
|
|
170
|
+
comments(): MorphMany<Comment, Post> {
|
|
171
|
+
return this.morphMany(Comment, 'commentable');
|
|
274
172
|
}
|
|
275
173
|
}
|
|
276
|
-
```
|
|
277
174
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const app = new Application(__dirname);
|
|
285
|
-
app.register(AppRouteServiceProvider);
|
|
286
|
-
await app.boot();
|
|
287
|
-
|
|
288
|
-
const kernel = new Kernel(app);
|
|
289
|
-
kernel.listen(3000);
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Middleware
|
|
293
|
-
|
|
294
|
-
Global and route-level middleware:
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
// Global middleware
|
|
298
|
-
kernel.use(async (req, res, next) => {
|
|
299
|
-
console.log(`${req.method} ${req.path}`);
|
|
300
|
-
await next();
|
|
301
|
-
});
|
|
175
|
+
export class Video extends Ensemble {
|
|
176
|
+
@DynamicRelation
|
|
177
|
+
comments(): MorphMany<Comment, Video> {
|
|
178
|
+
return this.morphMany(Comment, 'commentable');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
302
181
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
182
|
+
export class Comment extends Ensemble {
|
|
183
|
+
@DynamicRelation
|
|
184
|
+
commentable(): MorphTo<Post | Video> {
|
|
185
|
+
return this.morphTo('commentable');
|
|
307
186
|
}
|
|
308
|
-
|
|
309
|
-
};
|
|
187
|
+
}
|
|
310
188
|
|
|
311
|
-
|
|
189
|
+
// Use polymorphic relations
|
|
190
|
+
const post = await Post.find(1);
|
|
191
|
+
const comments = await post.comments;
|
|
192
|
+
|
|
193
|
+
const comment = await Comment.find(1);
|
|
194
|
+
const parent = await comment.commentable; // Returns Post or Video
|
|
312
195
|
```
|
|
313
196
|
|
|
314
|
-
|
|
197
|
+
## @DynamicRelation Decorator
|
|
315
198
|
|
|
316
|
-
|
|
199
|
+
The `@DynamicRelation` decorator enables dual-mode access to relationships:
|
|
317
200
|
|
|
318
201
|
```typescript
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// Authorize the request
|
|
324
|
-
protected authorize(): boolean {
|
|
325
|
-
return this.request.header('authorization') !== undefined;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Define validation rules
|
|
329
|
-
protected rules(): ValidationRules {
|
|
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
|
-
}
|
|
338
|
-
|
|
339
|
-
// Custom error messages
|
|
340
|
-
protected messages(): Record<string, string> {
|
|
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
|
-
};
|
|
202
|
+
export class User extends Ensemble {
|
|
203
|
+
@DynamicRelation
|
|
204
|
+
posts(): HasMany<Post, User> {
|
|
205
|
+
return this.hasMany(Post);
|
|
346
206
|
}
|
|
347
207
|
}
|
|
348
208
|
|
|
349
|
-
|
|
350
|
-
@Injectable()
|
|
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();
|
|
356
|
-
|
|
357
|
-
const user = await User.create(validated);
|
|
358
|
-
return res.status(201).json({ user });
|
|
359
|
-
}
|
|
360
|
-
}
|
|
209
|
+
const user = await User.find(1);
|
|
361
210
|
|
|
362
|
-
|
|
211
|
+
// Method syntax (returns query builder)
|
|
212
|
+
const query = user.posts();
|
|
213
|
+
const recentPosts = await query.where('created_at', '>', yesterday).get();
|
|
363
214
|
|
|
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
|
-
});
|
|
215
|
+
// Property syntax (returns results directly)
|
|
216
|
+
const allPosts = await user.posts;
|
|
376
217
|
```
|
|
377
218
|
|
|
378
|
-
|
|
219
|
+
Without `@DynamicRelation`, you must always call the method: `user.posts().get()`.
|
|
379
220
|
|
|
380
|
-
|
|
221
|
+
## Controllers
|
|
381
222
|
|
|
382
223
|
```typescript
|
|
383
|
-
import {
|
|
224
|
+
import { Controller, Injectable, ValidateRequest } from '@orchestr-sh/orchestr';
|
|
384
225
|
|
|
385
|
-
// Use @Injectable() when injecting dependencies
|
|
386
226
|
@Injectable()
|
|
387
|
-
class UserController extends Controller {
|
|
388
|
-
|
|
389
|
-
constructor(private userService: UserService) {
|
|
227
|
+
export class UserController extends Controller {
|
|
228
|
+
constructor(private service: UserService) {
|
|
390
229
|
super();
|
|
391
230
|
}
|
|
392
231
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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 });
|
|
232
|
+
@ValidateRequest()
|
|
233
|
+
async index(req: GetUsersRequest, res: any) {
|
|
234
|
+
const users = await User.query().with('posts').get();
|
|
235
|
+
return res.json({ data: users });
|
|
402
236
|
}
|
|
403
237
|
|
|
404
|
-
async
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
});
|
|
409
|
-
const user = await this.userService.create(validated);
|
|
410
|
-
return res.status(201).json({ user });
|
|
238
|
+
async show(req: any, res: any) {
|
|
239
|
+
const user = await User.find(req.routeParam('id'));
|
|
240
|
+
if (!user) return res.status(404).json({ error: 'Not found' });
|
|
241
|
+
return res.json({ data: user });
|
|
411
242
|
}
|
|
412
243
|
}
|
|
413
244
|
|
|
414
|
-
// Register
|
|
245
|
+
// Register route
|
|
415
246
|
Route.get('/users', [UserController, 'index']);
|
|
416
247
|
Route.get('/users/:id', [UserController, 'show']);
|
|
417
|
-
Route.post('/users', [UserController, 'store']);
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
**Note**: The `@Injectable()` decorator must be used on any class that needs constructor dependency injection. Without it, TypeScript won't emit the metadata needed for automatic resolution.
|
|
421
|
-
|
|
422
|
-
### Request
|
|
423
|
-
|
|
424
|
-
Powerful request helper methods:
|
|
425
|
-
|
|
426
|
-
```typescript
|
|
427
|
-
// Get input
|
|
428
|
-
req.input('name');
|
|
429
|
-
req.get('email', 'default@example.com');
|
|
430
|
-
|
|
431
|
-
// Get all inputs
|
|
432
|
-
req.all();
|
|
433
|
-
|
|
434
|
-
// Get specific inputs
|
|
435
|
-
req.only(['name', 'email']);
|
|
436
|
-
req.except(['password']);
|
|
437
|
-
|
|
438
|
-
// Check input existence
|
|
439
|
-
req.has('name');
|
|
440
|
-
req.filled('email');
|
|
441
|
-
|
|
442
|
-
// Route parameters
|
|
443
|
-
req.routeParam('id');
|
|
444
|
-
|
|
445
|
-
// Headers
|
|
446
|
-
req.header('content-type');
|
|
447
|
-
req.expectsJson();
|
|
448
|
-
req.ajax();
|
|
449
|
-
|
|
450
|
-
// Request info
|
|
451
|
-
req.method;
|
|
452
|
-
req.path;
|
|
453
|
-
req.ip();
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Response
|
|
457
|
-
|
|
458
|
-
Fluent response building:
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
// JSON responses
|
|
462
|
-
res.json({ data: [] });
|
|
463
|
-
res.status(201).json({ created: true });
|
|
464
|
-
|
|
465
|
-
// Headers
|
|
466
|
-
res.header('X-Custom', 'value');
|
|
467
|
-
res.headers({ 'X-A': 'a', 'X-B': 'b' });
|
|
468
|
-
|
|
469
|
-
// Cookies
|
|
470
|
-
res.cookie('token', 'value', { httpOnly: true, maxAge: 3600 });
|
|
471
|
-
|
|
472
|
-
// Redirects
|
|
473
|
-
res.redirect('/home');
|
|
474
|
-
res.redirect('/login', 301);
|
|
475
|
-
|
|
476
|
-
// Downloads
|
|
477
|
-
res.download(buffer, 'file.pdf');
|
|
478
|
-
|
|
479
|
-
// Views (simplified)
|
|
480
|
-
res.view('welcome', { name: 'John' });
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### Facades
|
|
484
|
-
|
|
485
|
-
Static access to services:
|
|
486
|
-
|
|
487
|
-
```typescript
|
|
488
|
-
import { Route, DB } from 'orchestr';
|
|
489
|
-
|
|
490
|
-
// Route facade provides static access to Router
|
|
491
|
-
Route.get('/path', handler);
|
|
492
|
-
Route.post('/path', handler);
|
|
493
|
-
Route.group({ prefix: 'api' }, () => {
|
|
494
|
-
// ...
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// DB facade provides static access to DatabaseManager
|
|
498
|
-
const users = await DB.table('users').where('active', true).get();
|
|
499
248
|
```
|
|
500
249
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
Fluent, chainable query builder with full Laravel API:
|
|
250
|
+
## FormRequest Validation
|
|
504
251
|
|
|
505
252
|
```typescript
|
|
506
|
-
import {
|
|
507
|
-
|
|
508
|
-
// Basic queries
|
|
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();
|
|
253
|
+
import { FormRequest, ValidationRules } from '@orchestr-sh/orchestr';
|
|
528
254
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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();
|
|
544
|
-
|
|
545
|
-
// Aggregates
|
|
546
|
-
const count = await DB.table('users').count();
|
|
547
|
-
const max = await DB.table('orders').max('price');
|
|
548
|
-
const min = await DB.table('orders').min('price');
|
|
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
|
-
});
|
|
557
|
-
|
|
558
|
-
// Updates
|
|
559
|
-
await DB.table('users')
|
|
560
|
-
.where('id', 1)
|
|
561
|
-
.update({ votes: 1 });
|
|
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();
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
### Ensemble ORM
|
|
575
|
-
|
|
576
|
-
ActiveRecord ORM (Eloquent equivalent) with relationships and advanced features:
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
import { Ensemble, HasOne, HasMany, BelongsTo, BelongsToMany, softDeletes } from 'orchestr';
|
|
580
|
-
|
|
581
|
-
// Define models with relationships
|
|
582
|
-
class User extends Ensemble {
|
|
583
|
-
protected table = 'users';
|
|
584
|
-
protected fillable = ['name', 'email', 'password'];
|
|
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);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// One-to-Many: User has many posts
|
|
597
|
-
posts(): HasMany<Post, User> {
|
|
598
|
-
return this.hasMany(Post);
|
|
255
|
+
export class StoreUserRequest extends FormRequest {
|
|
256
|
+
protected authorize(): boolean {
|
|
257
|
+
return true; // Add authorization logic
|
|
599
258
|
}
|
|
600
259
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
260
|
+
protected rules(): ValidationRules {
|
|
261
|
+
return {
|
|
262
|
+
name: 'required|string|min:3',
|
|
263
|
+
email: 'required|email',
|
|
264
|
+
password: 'required|min:8',
|
|
265
|
+
};
|
|
606
266
|
}
|
|
607
267
|
}
|
|
608
268
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
269
|
+
// Use with @ValidateRequest decorator
|
|
270
|
+
@Injectable()
|
|
271
|
+
export class UserController extends Controller {
|
|
272
|
+
@ValidateRequest()
|
|
273
|
+
async store(req: StoreUserRequest, res: any) {
|
|
274
|
+
const validated = req.validated();
|
|
275
|
+
const user = await User.create(validated);
|
|
276
|
+
return res.status(201).json({ data: user });
|
|
615
277
|
}
|
|
616
278
|
}
|
|
279
|
+
```
|
|
617
280
|
|
|
618
|
-
|
|
619
|
-
protected table = 'posts';
|
|
281
|
+
## Configuration
|
|
620
282
|
|
|
621
|
-
|
|
622
|
-
author(): BelongsTo<User, Post> {
|
|
623
|
-
return this.belongsTo(User, 'user_id');
|
|
624
|
-
}
|
|
283
|
+
### Database Setup
|
|
625
284
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return this.hasMany(Comment);
|
|
629
|
-
}
|
|
285
|
+
```typescript
|
|
286
|
+
import { DatabaseServiceProvider, DatabaseManager, DrizzleAdapter } from '@orchestr-sh/orchestr';
|
|
630
287
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
288
|
+
export class DatabaseServiceProvider extends ServiceProvider {
|
|
289
|
+
register(): void {
|
|
290
|
+
this.app.singleton('db', () => {
|
|
291
|
+
const config = this.app.make('config').get('database');
|
|
292
|
+
const manager = new DatabaseManager(config);
|
|
293
|
+
manager.registerAdapter('drizzle', (config) => new DrizzleAdapter(config));
|
|
294
|
+
return manager;
|
|
295
|
+
});
|
|
634
296
|
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
class Role extends Ensemble {
|
|
638
|
-
protected table = 'roles';
|
|
639
297
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
298
|
+
async boot(): Promise<void> {
|
|
299
|
+
const db = this.app.make('db');
|
|
300
|
+
await db.connection().connect();
|
|
301
|
+
Ensemble.setConnectionResolver(db);
|
|
643
302
|
}
|
|
644
303
|
}
|
|
645
304
|
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
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();
|
|
713
|
-
|
|
714
|
-
// Or use create
|
|
715
|
-
const user = await User.query().create({
|
|
716
|
-
name: 'John Doe',
|
|
717
|
-
email: 'john@example.com'
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
// Update
|
|
721
|
-
const user = await User.query().find(1);
|
|
722
|
-
user.name = 'Jane Doe';
|
|
723
|
-
await user.save();
|
|
724
|
-
|
|
725
|
-
// Delete
|
|
726
|
-
await user.delete();
|
|
727
|
-
|
|
728
|
-
// Soft deletes
|
|
729
|
-
class Article extends softDeletes(Ensemble) {
|
|
730
|
-
protected table = 'articles';
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const article = await Article.query().find(1);
|
|
734
|
-
await article.delete(); // Soft delete
|
|
735
|
-
await article.restore(); // Restore
|
|
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();
|
|
743
|
-
|
|
744
|
-
// Query only trashed
|
|
745
|
-
const trashedArticles = await Article.query().onlyTrashed().get();
|
|
305
|
+
// Register in your app
|
|
306
|
+
app.register(DatabaseServiceProvider);
|
|
307
|
+
```
|
|
746
308
|
|
|
747
|
-
|
|
748
|
-
// Automatically manages created_at and updated_at
|
|
749
|
-
class Post extends Ensemble {
|
|
750
|
-
protected table = 'posts';
|
|
751
|
-
public timestamps = true; // enabled by default
|
|
752
|
-
}
|
|
309
|
+
### Service Providers
|
|
753
310
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
protected casts = {
|
|
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')}`;
|
|
766
|
-
}
|
|
311
|
+
```typescript
|
|
312
|
+
import { ServiceProvider } from '@orchestr-sh/orchestr';
|
|
767
313
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
this.
|
|
314
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
315
|
+
register(): void {
|
|
316
|
+
this.app.singleton('myService', () => new MyService());
|
|
771
317
|
}
|
|
772
|
-
}
|
|
773
318
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
user.password = 'secret123'; // Uses mutator
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
#### Many-to-Many Relationships
|
|
780
|
-
|
|
781
|
-
Orchestr provides full support for many-to-many relationships with pivot table management, exactly like Laravel:
|
|
782
|
-
|
|
783
|
-
```typescript
|
|
784
|
-
class User extends Ensemble {
|
|
785
|
-
// Define many-to-many relationship
|
|
786
|
-
roles(): BelongsToMany<Role, User> {
|
|
787
|
-
return this.belongsToMany(Role, 'role_user')
|
|
788
|
-
.withPivot('expires_at', 'granted_by') // Additional pivot columns
|
|
789
|
-
.withTimestamps(); // Auto-manage timestamps
|
|
319
|
+
async boot(): Promise<void> {
|
|
320
|
+
// Bootstrap code
|
|
790
321
|
}
|
|
791
322
|
}
|
|
792
|
-
|
|
793
|
-
const user = await User.find(1);
|
|
794
|
-
|
|
795
|
-
// Attach roles
|
|
796
|
-
await user.roles().attach([1, 2, 3]);
|
|
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 }
|
|
830
323
|
```
|
|
831
324
|
|
|
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
|
|
325
|
+
## API Reference
|
|
843
326
|
|
|
844
|
-
|
|
327
|
+
### Ensemble Methods
|
|
845
328
|
|
|
846
329
|
```typescript
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
},
|
|
861
|
-
postgres: {
|
|
862
|
-
adapter: 'drizzle',
|
|
863
|
-
client: drizzle(process.env.DATABASE_URL!)
|
|
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();
|
|
330
|
+
// Query
|
|
331
|
+
User.query() // Get query builder
|
|
332
|
+
User.find(id) // Find by primary key
|
|
333
|
+
User.findOrFail(id) // Find or throw error
|
|
334
|
+
User.all() // Get all records
|
|
335
|
+
User.create(data) // Create and save
|
|
336
|
+
|
|
337
|
+
// Instance methods
|
|
338
|
+
user.save() // Save changes
|
|
339
|
+
user.delete() // Delete record
|
|
340
|
+
user.refresh() // Reload from database
|
|
341
|
+
user.load('posts') // Lazy load relationship
|
|
342
|
+
user.toObject() // Convert to plain object
|
|
875
343
|
```
|
|
876
344
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
Here's a complete example showing routing, database, and ORM:
|
|
345
|
+
### Query Builder Methods
|
|
880
346
|
|
|
881
|
-
**index.ts**
|
|
882
347
|
```typescript
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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);
|
|
348
|
+
.where(column, value)
|
|
349
|
+
.where(column, operator, value)
|
|
350
|
+
.orWhere(column, value)
|
|
351
|
+
.whereIn(column, array)
|
|
352
|
+
.whereBetween(column, [min, max])
|
|
353
|
+
.whereNull(column)
|
|
354
|
+
.orderBy(column, direction)
|
|
355
|
+
.limit(number)
|
|
356
|
+
.offset(number)
|
|
357
|
+
.join(table, first, operator, second)
|
|
358
|
+
.groupBy(column)
|
|
359
|
+
.having(column, operator, value)
|
|
360
|
+
.select(columns)
|
|
361
|
+
.count()
|
|
362
|
+
.sum(column)
|
|
363
|
+
.avg(column)
|
|
364
|
+
.min(column)
|
|
365
|
+
.max(column)
|
|
909
366
|
```
|
|
910
367
|
|
|
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'];
|
|
368
|
+
### Relationship Methods
|
|
920
369
|
|
|
921
|
-
protected casts = {
|
|
922
|
-
email_verified_at: 'datetime',
|
|
923
|
-
is_admin: 'boolean'
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
// Define relationship
|
|
927
|
-
posts(): HasMany<Post, User> {
|
|
928
|
-
return this.hasMany(Post);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
**app/Models/Post.ts**
|
|
934
|
-
```typescript
|
|
935
|
-
import { Ensemble, BelongsTo } from 'orchestr';
|
|
936
|
-
import { User } from './User';
|
|
937
|
-
|
|
938
|
-
export class Post extends Ensemble {
|
|
939
|
-
protected table = 'posts';
|
|
940
|
-
protected fillable = ['user_id', 'title', 'content', 'published_at'];
|
|
941
|
-
|
|
942
|
-
author(): BelongsTo<User, Post> {
|
|
943
|
-
return this.belongsTo(User, 'user_id');
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
**routes/api.ts**
|
|
949
370
|
```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
|
-
});
|
|
371
|
+
// HasOne, HasMany, BelongsTo
|
|
372
|
+
.get() // Execute query
|
|
373
|
+
.first() // Get first result
|
|
374
|
+
.create(data) // Create related model
|
|
375
|
+
.where(column, value) // Add constraint
|
|
376
|
+
|
|
377
|
+
// BelongsToMany
|
|
378
|
+
.attach(ids) // Attach related models
|
|
379
|
+
.detach(ids) // Detach related models
|
|
380
|
+
.sync(ids) // Sync relationships
|
|
381
|
+
.toggle(ids) // Toggle relationships
|
|
382
|
+
.wherePivot(column, value) // Query pivot table
|
|
383
|
+
.updateExistingPivot(id, data) // Update pivot data
|
|
384
|
+
|
|
385
|
+
// All relationships
|
|
386
|
+
.with('relation') // Eager load
|
|
387
|
+
.with(['relation1', 'relation2'])
|
|
388
|
+
.with({ relation: (q) => q.where(...) })
|
|
992
389
|
```
|
|
993
390
|
|
|
994
|
-
|
|
391
|
+
### Available Relationships
|
|
995
392
|
|
|
996
|
-
|
|
393
|
+
- `HasOne` - One-to-one
|
|
394
|
+
- `HasMany` - One-to-many
|
|
395
|
+
- `BelongsTo` - Inverse of HasOne/HasMany
|
|
396
|
+
- `BelongsToMany` - Many-to-many
|
|
397
|
+
- `MorphOne` - Polymorphic one-to-one
|
|
398
|
+
- `MorphMany` - Polymorphic one-to-many
|
|
399
|
+
- `MorphTo` - Inverse of MorphOne/MorphMany
|
|
400
|
+
- `MorphToMany` - Polymorphic many-to-many
|
|
401
|
+
- `MorphedByMany` - Inverse of MorphToMany
|
|
997
402
|
|
|
998
|
-
|
|
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
|
-
```
|
|
403
|
+
## Features
|
|
1046
404
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
-
|
|
1052
|
-
-
|
|
1053
|
-
-
|
|
1054
|
-
-
|
|
1055
|
-
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
- [x] Service Container & Dependency Injection
|
|
1062
|
-
- [x] Service Providers
|
|
1063
|
-
- [x] Configuration System
|
|
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 | ✅ | 🚧 |
|
|
405
|
+
- ✅ Service Container & Dependency Injection
|
|
406
|
+
- ✅ Configuration System
|
|
407
|
+
- ✅ HTTP Router & Middleware
|
|
408
|
+
- ✅ Controllers with DI
|
|
409
|
+
- ✅ FormRequest Validation
|
|
410
|
+
- ✅ Query Builder
|
|
411
|
+
- ✅ Ensemble ORM (ActiveRecord)
|
|
412
|
+
- ✅ Relationships (Standard + Polymorphic)
|
|
413
|
+
- ✅ Eager/Lazy Loading
|
|
414
|
+
- ✅ Soft Deletes
|
|
415
|
+
- ✅ Attribute Casting
|
|
416
|
+
- ✅ Timestamps
|
|
417
|
+
- ✅ @DynamicRelation Decorator
|
|
1124
418
|
|
|
1125
419
|
## License
|
|
1126
420
|
|
|
1127
421
|
MIT
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
Built with TypeScript. Inspired by Laravel.
|