bunsane 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy-docs.yml +57 -0
- package/LICENSE.md +1 -1
- package/README.md +2 -28
- package/TODO.md +8 -1
- package/bun.lock +3 -0
- package/config/upload.config.ts +135 -0
- package/core/App.ts +168 -4
- package/core/ArcheType.ts +122 -0
- package/core/BatchLoader.ts +100 -0
- package/core/ComponentRegistry.ts +4 -3
- package/core/Components.ts +2 -2
- package/core/Decorators.ts +15 -8
- package/core/Entity.ts +193 -14
- package/core/EntityCache.ts +15 -0
- package/core/EntityHookManager.ts +855 -0
- package/core/EntityManager.ts +12 -2
- package/core/ErrorHandler.ts +64 -7
- package/core/FileValidator.ts +284 -0
- package/core/Query.ts +503 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +89 -0
- package/core/SchedulerManager.ts +710 -0
- package/core/UploadManager.ts +261 -0
- package/core/components/UploadComponent.ts +206 -0
- package/core/decorators/EntityHooks.ts +190 -0
- package/core/decorators/ScheduledTask.ts +83 -0
- package/core/events/EntityLifecycleEvents.ts +177 -0
- package/core/processors/ImageProcessor.ts +423 -0
- package/core/storage/LocalStorageProvider.ts +290 -0
- package/core/storage/StorageProvider.ts +112 -0
- package/database/DatabaseHelper.ts +183 -58
- package/database/index.ts +5 -5
- package/database/sqlHelpers.ts +7 -0
- package/docs/README.md +149 -0
- package/docs/_coverpage.md +36 -0
- package/docs/_sidebar.md +23 -0
- package/docs/api/core.md +568 -0
- package/docs/api/hooks.md +554 -0
- package/docs/api/index.md +222 -0
- package/docs/api/query.md +678 -0
- package/docs/api/service.md +744 -0
- package/docs/core-concepts/archetypes.md +512 -0
- package/docs/core-concepts/components.md +498 -0
- package/docs/core-concepts/entity.md +314 -0
- package/docs/core-concepts/hooks.md +683 -0
- package/docs/core-concepts/query.md +588 -0
- package/docs/core-concepts/services.md +647 -0
- package/docs/examples/code-examples.md +425 -0
- package/docs/getting-started.md +337 -0
- package/docs/index.html +97 -0
- package/gql/Generator.ts +58 -35
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +65 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +19 -10
- package/rest/Generator.ts +3 -0
- package/rest/index.ts +22 -0
- package/service/Service.ts +1 -1
- package/service/ServiceRegistry.ts +10 -6
- package/service/index.ts +12 -1
- package/tests/bench/insert.bench.ts +59 -0
- package/tests/bench/relations.bench.ts +269 -0
- package/tests/bench/sorting.bench.ts +415 -0
- package/tests/component-hooks.test.ts +1409 -0
- package/tests/component.test.ts +338 -0
- package/tests/errorHandling.test.ts +155 -0
- package/tests/hooks.test.ts +666 -0
- package/tests/query-sorting.test.ts +101 -0
- package/tests/relations.test.ts +169 -0
- package/tests/scheduler.test.ts +724 -0
- package/tsconfig.json +35 -34
- package/types/graphql.types.ts +87 -0
- package/types/hooks.types.ts +141 -0
- package/types/scheduler.types.ts +165 -0
- package/types/upload.types.ts +184 -0
- package/upload/index.ts +140 -0
- package/utils/UploadHelper.ts +305 -0
- package/utils/cronParser.ts +366 -0
- package/utils/errorMessages.ts +151 -0
- package/core/Events.ts +0 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
# Service API Reference
|
|
2
|
+
|
|
3
|
+
This page provides detailed API reference for BunSane's service layer and business logic components.
|
|
4
|
+
|
|
5
|
+
## 🏢 BaseService Class
|
|
6
|
+
|
|
7
|
+
The `BaseService` class provides the foundation for all business logic services in BunSane.
|
|
8
|
+
|
|
9
|
+
### Constructor
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
new BaseService()
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Instance Methods
|
|
16
|
+
|
|
17
|
+
#### `service.initialize()`
|
|
18
|
+
|
|
19
|
+
Initializes the service with dependencies.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
async initialize(): Promise<void>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Returns:** `Promise<void>`
|
|
26
|
+
|
|
27
|
+
**Example:**
|
|
28
|
+
```typescript
|
|
29
|
+
await userService.initialize();
|
|
30
|
+
// Service is now ready to use
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 👤 ServiceRegistry Class
|
|
34
|
+
|
|
35
|
+
Manages service registration and dependency injection. Services are automatically registered when they extend `BaseService` and are imported in your application.
|
|
36
|
+
|
|
37
|
+
### Static Methods
|
|
38
|
+
|
|
39
|
+
#### `ServiceRegistry.register(serviceClass)`
|
|
40
|
+
|
|
41
|
+
Registers a service with the registry.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
static register(serviceClass: new () => BaseService): void
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Parameters:**
|
|
48
|
+
- `serviceClass`: Service constructor
|
|
49
|
+
|
|
50
|
+
**Example:**
|
|
51
|
+
```typescript
|
|
52
|
+
export class UserService extends BaseService {
|
|
53
|
+
// Service implementation
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Automatic registration when imported
|
|
57
|
+
// No manual registration needed in most cases
|
|
58
|
+
ServiceRegistry.register(UserService);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### `ServiceRegistry.get(serviceClass)`
|
|
62
|
+
|
|
63
|
+
Gets a service instance by class.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
static get<T extends BaseService>(serviceClass: new () => T): T
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Type Parameters:**
|
|
70
|
+
- `T`: Service class
|
|
71
|
+
|
|
72
|
+
**Parameters:**
|
|
73
|
+
- `serviceClass`: Service constructor
|
|
74
|
+
|
|
75
|
+
**Returns:** `T` - Service instance
|
|
76
|
+
|
|
77
|
+
**Example:**
|
|
78
|
+
```typescript
|
|
79
|
+
const userService = ServiceRegistry.get(UserService);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Automatic Service Discovery
|
|
83
|
+
|
|
84
|
+
Services are automatically discovered and registered when:
|
|
85
|
+
1. They extend `BaseService`
|
|
86
|
+
2. They are imported in your application entry point
|
|
87
|
+
3. The application starts via `App.start()`
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// app.ts
|
|
91
|
+
import { App } from 'bunsane';
|
|
92
|
+
import { UserService } from './services/UserService';
|
|
93
|
+
import { OrderService } from './services/OrderService';
|
|
94
|
+
|
|
95
|
+
const app = new App();
|
|
96
|
+
|
|
97
|
+
// Services are automatically registered when imported
|
|
98
|
+
// No manual registration required
|
|
99
|
+
app.start();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Service Dependencies
|
|
103
|
+
|
|
104
|
+
Services can access other services through the ServiceRegistry:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
export class OrderService extends BaseService {
|
|
108
|
+
private userService: UserService;
|
|
109
|
+
|
|
110
|
+
async initialize(): Promise<void> {
|
|
111
|
+
await super.initialize();
|
|
112
|
+
// Get service instance from registry
|
|
113
|
+
this.userService = ServiceRegistry.get(UserService);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async createOrder(userId: string, orderData: any) {
|
|
117
|
+
// Validate user exists
|
|
118
|
+
const user = await this.userService.getUser({ id: userId });
|
|
119
|
+
if (!user) throw new Error('User not found');
|
|
120
|
+
|
|
121
|
+
// Create order logic
|
|
122
|
+
const order = Entity.Create();
|
|
123
|
+
// ... order creation logic
|
|
124
|
+
return order;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 🎯 GraphQL Service Decorators
|
|
130
|
+
|
|
131
|
+
### @GraphQLObjectType
|
|
132
|
+
|
|
133
|
+
Defines a GraphQL object type for the service.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
@GraphQLObjectType(config: GraphQLObjectTypeConfig): ClassDecorator
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Parameters:**
|
|
140
|
+
- `config`: GraphQLObjectTypeConfig - Type configuration
|
|
141
|
+
|
|
142
|
+
**Example:**
|
|
143
|
+
```typescript
|
|
144
|
+
const userFields = {
|
|
145
|
+
id: GraphQLFieldTypes.ID_REQUIRED,
|
|
146
|
+
name: GraphQLFieldTypes.STRING_OPTIONAL,
|
|
147
|
+
email: GraphQLFieldTypes.STRING_REQUIRED
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
@GraphQLObjectType({
|
|
151
|
+
name: "User",
|
|
152
|
+
fields: userFields
|
|
153
|
+
})
|
|
154
|
+
export class UserService extends BaseService {
|
|
155
|
+
// Service implementation
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### @GraphQLOperation
|
|
160
|
+
|
|
161
|
+
Defines a GraphQL operation (Query/Mutation).
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
@GraphQLOperation(config: GraphQLOperationConfig): MethodDecorator
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Parameters:**
|
|
168
|
+
- `config`: GraphQLOperationConfig - Operation configuration
|
|
169
|
+
|
|
170
|
+
**Example:**
|
|
171
|
+
```typescript
|
|
172
|
+
@GraphQLOperation({
|
|
173
|
+
type: "Query",
|
|
174
|
+
input: { id: GraphQLFieldTypes.ID_REQUIRED },
|
|
175
|
+
output: "User"
|
|
176
|
+
})
|
|
177
|
+
async getUser(args: { id: string }) {
|
|
178
|
+
// Implementation
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@GraphQLOperation({
|
|
182
|
+
type: "Mutation",
|
|
183
|
+
input: {
|
|
184
|
+
name: GraphQLFieldTypes.STRING_REQUIRED,
|
|
185
|
+
email: GraphQLFieldTypes.STRING_REQUIRED
|
|
186
|
+
},
|
|
187
|
+
output: "User"
|
|
188
|
+
})
|
|
189
|
+
async createUser(args: { name: string; email: string }) {
|
|
190
|
+
// Implementation
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### @GraphQLField
|
|
195
|
+
|
|
196
|
+
Defines a GraphQL field resolver.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
@GraphQLField(config: GraphQLFieldConfig): MethodDecorator
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Parameters:**
|
|
203
|
+
- `config`: GraphQLFieldConfig - Field configuration
|
|
204
|
+
|
|
205
|
+
**Example:**
|
|
206
|
+
```typescript
|
|
207
|
+
@GraphQLField({ type: "User", field: "name" })
|
|
208
|
+
async nameResolver(parent: Entity) {
|
|
209
|
+
const profile = await parent.get(UserProfile);
|
|
210
|
+
return profile?.name ?? "";
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 🌐 REST Service Decorators
|
|
215
|
+
|
|
216
|
+
### @Post
|
|
217
|
+
|
|
218
|
+
Defines a POST REST endpoint.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
@Post(path: string): MethodDecorator
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Parameters:**
|
|
225
|
+
- `path`: String - Endpoint path
|
|
226
|
+
|
|
227
|
+
**Example:**
|
|
228
|
+
```typescript
|
|
229
|
+
@Post("/auth/login")
|
|
230
|
+
async userLogin(req: Request) {
|
|
231
|
+
// Handle login logic
|
|
232
|
+
return new Response(JSON.stringify({ token: "jwt-token" }));
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## ⏰ Scheduled Task Decorators
|
|
237
|
+
|
|
238
|
+
### @ScheduledTask
|
|
239
|
+
|
|
240
|
+
Defines a scheduled background task.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
@ScheduledTask(config: ScheduledTaskConfig): MethodDecorator
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Parameters:**
|
|
247
|
+
- `config`: ScheduledTaskConfig - Task configuration
|
|
248
|
+
|
|
249
|
+
**Example:**
|
|
250
|
+
```typescript
|
|
251
|
+
@ScheduledTask({
|
|
252
|
+
interval: ScheduleInterval.MINUTE,
|
|
253
|
+
componentTarget: {
|
|
254
|
+
includeComponents: [UserTag],
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
async checkUserPerMinutes(entities: Entity[]) {
|
|
258
|
+
// Run every minute for user entities
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## 🎣 Lifecycle Hook Decorators
|
|
263
|
+
|
|
264
|
+
### @ComponentTargetHook
|
|
265
|
+
|
|
266
|
+
Defines a component lifecycle hook.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
@ComponentTargetHook(event: string, config: HookConfig): MethodDecorator
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Parameters:**
|
|
273
|
+
- `event`: String - Hook event name
|
|
274
|
+
- `config`: HookConfig - Hook configuration
|
|
275
|
+
|
|
276
|
+
**Example:**
|
|
277
|
+
```typescript
|
|
278
|
+
@ComponentTargetHook("entity.created", {
|
|
279
|
+
includeComponents: [UserTag, EmailComponent]
|
|
280
|
+
})
|
|
281
|
+
async onUserCreate(event: EntityCreatedEvent) {
|
|
282
|
+
const emailComp = await event.entity.get(EmailComponent);
|
|
283
|
+
console.log(`New user: ${emailComp?.value}`);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 📋 Service Patterns
|
|
288
|
+
|
|
289
|
+
### GraphQL CRUD Service Pattern
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { GraphQLObjectType, GraphQLOperation, GraphQLField, GraphQLFieldTypes } from 'bunsane';
|
|
293
|
+
|
|
294
|
+
const userFields = {
|
|
295
|
+
id: GraphQLFieldTypes.ID_REQUIRED,
|
|
296
|
+
name: GraphQLFieldTypes.STRING_OPTIONAL,
|
|
297
|
+
email: GraphQLFieldTypes.STRING_REQUIRED,
|
|
298
|
+
username: GraphQLFieldTypes.STRING_OPTIONAL
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const userInputs = {
|
|
302
|
+
createUser: {
|
|
303
|
+
name: GraphQLFieldTypes.STRING_REQUIRED,
|
|
304
|
+
email: GraphQLFieldTypes.STRING_REQUIRED,
|
|
305
|
+
username: GraphQLFieldTypes.STRING_REQUIRED
|
|
306
|
+
},
|
|
307
|
+
getUser: {
|
|
308
|
+
id: GraphQLFieldTypes.ID_REQUIRED
|
|
309
|
+
},
|
|
310
|
+
updateUser: {
|
|
311
|
+
id: GraphQLFieldTypes.ID_REQUIRED,
|
|
312
|
+
name: GraphQLFieldTypes.STRING_OPTIONAL,
|
|
313
|
+
email: GraphQLFieldTypes.STRING_OPTIONAL
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
@GraphQLObjectType({
|
|
318
|
+
name: "User",
|
|
319
|
+
fields: userFields
|
|
320
|
+
})
|
|
321
|
+
export class UserService extends BaseService {
|
|
322
|
+
@GraphQLOperation({
|
|
323
|
+
type: "Mutation",
|
|
324
|
+
input: userInputs.createUser,
|
|
325
|
+
output: "User"
|
|
326
|
+
})
|
|
327
|
+
async createUser(args: { name: string; email: string; username: string }) {
|
|
328
|
+
const userEntity = UserArcheType.fill(args).createEntity();
|
|
329
|
+
await userEntity.save();
|
|
330
|
+
return await UserArcheType.Unwrap(userEntity);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@GraphQLOperation({
|
|
334
|
+
type: "Query",
|
|
335
|
+
input: userInputs.getUser,
|
|
336
|
+
output: "User"
|
|
337
|
+
})
|
|
338
|
+
async getUser(args: { id: string }) {
|
|
339
|
+
const entity = await Entity.FindById(args.id);
|
|
340
|
+
if (!entity) return null;
|
|
341
|
+
return await UserArcheType.Unwrap(entity);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@GraphQLOperation({
|
|
345
|
+
type: "Mutation",
|
|
346
|
+
input: userInputs.updateUser,
|
|
347
|
+
output: "User"
|
|
348
|
+
})
|
|
349
|
+
async updateUser(args: { id: string; name?: string; email?: string }) {
|
|
350
|
+
const entity = await Entity.FindById(args.id);
|
|
351
|
+
if (!entity) throw new Error('User not found');
|
|
352
|
+
|
|
353
|
+
await UserArcheType.updateEntity(entity, args);
|
|
354
|
+
await entity.save();
|
|
355
|
+
return await UserArcheType.Unwrap(entity);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@GraphQLField({ type: "User", field: "id" })
|
|
359
|
+
idResolver(parent: Entity) {
|
|
360
|
+
return parent.id;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@GraphQLField({ type: "User", field: "name" })
|
|
364
|
+
async nameResolver(parent: Entity) {
|
|
365
|
+
const profile = await parent.get(UserProfile);
|
|
366
|
+
return profile?.name ?? "";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@GraphQLField({ type: "User", field: "email" })
|
|
370
|
+
async emailResolver(parent: Entity) {
|
|
371
|
+
const profile = await parent.get(UserProfile);
|
|
372
|
+
return profile?.email ?? "";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@GraphQLField({ type: "User", field: "username" })
|
|
376
|
+
async usernameResolver(parent: Entity) {
|
|
377
|
+
const profile = await parent.get(UserProfile);
|
|
378
|
+
return profile?.username ?? "";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### REST Service Pattern
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import { Post } from 'bunsane';
|
|
387
|
+
|
|
388
|
+
export class AuthService extends BaseService {
|
|
389
|
+
@Post("/auth/login")
|
|
390
|
+
async userLogin(req: Request) {
|
|
391
|
+
const body = await req.json();
|
|
392
|
+
const { email, password } = body;
|
|
393
|
+
|
|
394
|
+
// Authentication logic
|
|
395
|
+
const user = await this.authenticateUser(email, password);
|
|
396
|
+
if (!user) {
|
|
397
|
+
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
398
|
+
status: 401
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const token = this.generateToken(user);
|
|
403
|
+
return new Response(JSON.stringify({ token, user }), { status: 200 });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@Post("/auth/register")
|
|
407
|
+
async userRegister(req: Request) {
|
|
408
|
+
try {
|
|
409
|
+
const body = await req.json();
|
|
410
|
+
const input = this.validateRegistrationData(body);
|
|
411
|
+
|
|
412
|
+
const existingUser = await Query.Find(UserTag)
|
|
413
|
+
.with(EmailComponent, Query.filters(
|
|
414
|
+
Query.filter("value", Query.filterOp.EQ, input.email)
|
|
415
|
+
))
|
|
416
|
+
.exec();
|
|
417
|
+
|
|
418
|
+
if (existingUser.length > 0) {
|
|
419
|
+
return new Response(JSON.stringify({
|
|
420
|
+
error: "Email already in use"
|
|
421
|
+
}), { status: 400 });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const entity = UserArcheType.fill(input).createEntity();
|
|
425
|
+
await entity.save();
|
|
426
|
+
|
|
427
|
+
return new Response(JSON.stringify({
|
|
428
|
+
message: "User registered successfully",
|
|
429
|
+
user: await UserArcheType.Unwrap(entity, ['password'])
|
|
430
|
+
}), { status: 201 });
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return new Response(JSON.stringify({
|
|
433
|
+
error: "Registration failed"
|
|
434
|
+
}), { status: 500 });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async authenticateUser(email: string, password: string) {
|
|
439
|
+
// Authentication implementation
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private generateToken(user: any) {
|
|
444
|
+
// JWT token generation
|
|
445
|
+
return "jwt-token";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private validateRegistrationData(data: any) {
|
|
449
|
+
// Validation logic
|
|
450
|
+
return data;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Scheduled Task Service Pattern
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
import { ScheduledTask, ScheduleInterval, ComponentTargetHook } from 'bunsane';
|
|
459
|
+
|
|
460
|
+
export class MaintenanceService extends BaseService {
|
|
461
|
+
@ScheduledTask({
|
|
462
|
+
interval: ScheduleInterval.HOUR,
|
|
463
|
+
componentTarget: {
|
|
464
|
+
includeComponents: [UserTag],
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
async cleanupInactiveUsers(entities: Entity[]) {
|
|
468
|
+
const oneMonthAgo = new Date();
|
|
469
|
+
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
470
|
+
|
|
471
|
+
for (const entity of entities) {
|
|
472
|
+
const lastLogin = await entity.get(LastLoginComponent);
|
|
473
|
+
if (lastLogin && lastLogin.value < oneMonthAgo) {
|
|
474
|
+
// Mark user as inactive or send reminder
|
|
475
|
+
await this.sendInactivityReminder(entity);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@ScheduledTask({
|
|
481
|
+
interval: ScheduleInterval.DAY,
|
|
482
|
+
componentTarget: {
|
|
483
|
+
includeComponents: [PostTag],
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
async cleanupOldPosts(entities: Entity[]) {
|
|
487
|
+
const thirtyDaysAgo = new Date();
|
|
488
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
489
|
+
|
|
490
|
+
for (const entity of entities) {
|
|
491
|
+
const createdAt = await entity.get(DateComponent);
|
|
492
|
+
if (createdAt && createdAt.value < thirtyDaysAgo) {
|
|
493
|
+
// Archive or delete old posts
|
|
494
|
+
await entity.delete();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@ComponentTargetHook("entity.created", {
|
|
500
|
+
includeComponents: [UserTag, EmailComponent]
|
|
501
|
+
})
|
|
502
|
+
async onUserCreated(event: EntityCreatedEvent) {
|
|
503
|
+
const emailComp = await event.entity.get(EmailComponent);
|
|
504
|
+
if (emailComp) {
|
|
505
|
+
await this.sendWelcomeEmail(emailComp.value);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async sendInactivityReminder(entity: Entity) {
|
|
510
|
+
// Send reminder email implementation
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private async sendWelcomeEmail(email: string) {
|
|
514
|
+
// Send welcome email implementation
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## 🔄 Service Communication
|
|
520
|
+
|
|
521
|
+
### Service Dependencies
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
export class NotificationService extends BaseService {
|
|
525
|
+
private emailService: EmailService;
|
|
526
|
+
private smsService: SmsService;
|
|
527
|
+
|
|
528
|
+
async initialize(): Promise<void> {
|
|
529
|
+
await super.initialize();
|
|
530
|
+
this.emailService = ServiceRegistry.get(EmailService);
|
|
531
|
+
this.smsService = ServiceRegistry.get(SmsService);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async sendWelcomeMessage(userId: string): Promise<void> {
|
|
535
|
+
const user = await Entity.FindById(userId);
|
|
536
|
+
if (!user) return;
|
|
537
|
+
|
|
538
|
+
const profile = await user.get(UserProfile);
|
|
539
|
+
|
|
540
|
+
// Send both email and SMS
|
|
541
|
+
await Promise.all([
|
|
542
|
+
this.emailService.sendWelcomeEmail(profile.email, profile.name),
|
|
543
|
+
this.smsService.sendWelcomeSms(profile.phone, profile.name)
|
|
544
|
+
]);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Event-Driven Services
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
export class AuditService extends BaseService {
|
|
553
|
+
async initialize(): Promise<void> {
|
|
554
|
+
await super.initialize();
|
|
555
|
+
|
|
556
|
+
// Listen to entity events
|
|
557
|
+
EntityHookManager.on('entity:created', this.onEntityCreated.bind(this));
|
|
558
|
+
EntityHookManager.on('entity:updated', this.onEntityUpdated.bind(this));
|
|
559
|
+
EntityHookManager.on('entity:deleted', this.onEntityDeleted.bind(this));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private async onEntityCreated(entity: Entity): Promise<void> {
|
|
563
|
+
await this.logAuditEvent({
|
|
564
|
+
action: 'CREATE',
|
|
565
|
+
entityId: entity.id,
|
|
566
|
+
timestamp: new Date(),
|
|
567
|
+
userId: this.getCurrentUserId()
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private async onEntityUpdated(entity: Entity): Promise<void> {
|
|
572
|
+
await this.logAuditEvent({
|
|
573
|
+
action: 'UPDATE',
|
|
574
|
+
entityId: entity.id,
|
|
575
|
+
timestamp: new Date(),
|
|
576
|
+
userId: this.getCurrentUserId()
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private async onEntityDeleted(entity: Entity): Promise<void> {
|
|
581
|
+
await this.logAuditEvent({
|
|
582
|
+
action: 'DELETE',
|
|
583
|
+
entityId: entity.id,
|
|
584
|
+
timestamp: new Date(),
|
|
585
|
+
userId: this.getCurrentUserId()
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private async logAuditEvent(event: AuditEvent): Promise<void> {
|
|
590
|
+
const auditEntity = Entity.Create();
|
|
591
|
+
await auditEntity.add(AuditLog, event);
|
|
592
|
+
await auditEntity.save();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private getCurrentUserId(): string {
|
|
596
|
+
// Get current user from context
|
|
597
|
+
return RequestContext.getCurrentUser()?.id || 'system';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## 🛡️ Error Handling
|
|
603
|
+
|
|
604
|
+
### Service Error Types
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
export class ServiceError extends Error {
|
|
608
|
+
constructor(
|
|
609
|
+
message: string,
|
|
610
|
+
public code: string,
|
|
611
|
+
public statusCode: number = 500
|
|
612
|
+
) {
|
|
613
|
+
super(message);
|
|
614
|
+
this.name = 'ServiceError';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export class ValidationError extends ServiceError {
|
|
619
|
+
constructor(message: string, public field?: string) {
|
|
620
|
+
super(message, 'VALIDATION_ERROR', 400);
|
|
621
|
+
this.name = 'ValidationError';
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export class NotFoundError extends ServiceError {
|
|
626
|
+
constructor(resource: string) {
|
|
627
|
+
super(`${resource} not found`, 'NOT_FOUND', 404);
|
|
628
|
+
this.name = 'NotFoundError';
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Error Handling Patterns
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
export class UserService extends BaseService {
|
|
637
|
+
async createUser(userData: CreateUserData): Promise<Entity> {
|
|
638
|
+
try {
|
|
639
|
+
// Validate input
|
|
640
|
+
this.validateUserData(userData);
|
|
641
|
+
|
|
642
|
+
// Check for existing user
|
|
643
|
+
const existing = await Query.Find(UserProfile)
|
|
644
|
+
.where({ email: userData.email })
|
|
645
|
+
.first();
|
|
646
|
+
|
|
647
|
+
if (existing) {
|
|
648
|
+
throw new ValidationError('Email already exists', 'email');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Create user
|
|
652
|
+
const user = Entity.Create();
|
|
653
|
+
await user.add(UserProfile, userData);
|
|
654
|
+
await user.save();
|
|
655
|
+
|
|
656
|
+
return user;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
this.getLogger().error('Failed to create user', { error, userData });
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private validateUserData(data: CreateUserData): void {
|
|
664
|
+
if (!data.email || !data.email.includes('@')) {
|
|
665
|
+
throw new ValidationError('Invalid email address', 'email');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (!data.name || data.name.length < 2) {
|
|
669
|
+
throw new ValidationError('Name must be at least 2 characters', 'name');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
## 🚀 Performance Optimization
|
|
676
|
+
|
|
677
|
+
### Service Caching
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
export class CacheService extends BaseService {
|
|
681
|
+
private cache = new Map<string, any>();
|
|
682
|
+
|
|
683
|
+
async get<T>(key: string, ttl: number = 300000): Promise<T | null> {
|
|
684
|
+
const cached = this.cache.get(key);
|
|
685
|
+
if (cached && cached.expires > Date.now()) {
|
|
686
|
+
return cached.value;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async set<T>(key: string, value: T, ttl: number = 300000): Promise<void> {
|
|
692
|
+
this.cache.set(key, {
|
|
693
|
+
value,
|
|
694
|
+
expires: Date.now() + ttl
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
clear(): void {
|
|
699
|
+
this.cache.clear();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Batch Operations
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
export class BulkOperationService extends BaseService {
|
|
708
|
+
async bulkCreateUsers(userData: CreateUserData[]): Promise<Entity[]> {
|
|
709
|
+
const entities: Entity[] = [];
|
|
710
|
+
|
|
711
|
+
// Process in batches to avoid memory issues
|
|
712
|
+
const batchSize = 100;
|
|
713
|
+
for (let i = 0; i < userData.length; i += batchSize) {
|
|
714
|
+
const batch = userData.slice(i, i + batchSize);
|
|
715
|
+
const batchEntities = await this.createUserBatch(batch);
|
|
716
|
+
entities.push(...batchEntities);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return entities;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private async createUserBatch(batch: CreateUserData[]): Promise<Entity[]> {
|
|
723
|
+
const entities = batch.map(data => {
|
|
724
|
+
const entity = Entity.Create();
|
|
725
|
+
await entity.add(UserProfile, data);
|
|
726
|
+
return entity;
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Save all entities in parallel
|
|
730
|
+
await Promise.all(entities.map(entity => entity.save()));
|
|
731
|
+
return entities;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
## 🔗 Related APIs
|
|
737
|
+
|
|
738
|
+
- **[Entity API](core.md)** - Entity operations
|
|
739
|
+
- **[Query API](query.md)** - Database querying
|
|
740
|
+
- **[Hooks API](hooks.md)** - Lifecycle events
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
*Need more details? Check the [Hooks API](hooks.md) for lifecycle event handling!* 🚀
|