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,647 @@
|
|
|
1
|
+
# Service System
|
|
2
|
+
|
|
3
|
+
Services are BunSane's business logic layer that provide a clean separation between your application's logic and data layer. Services extend `BaseService` and can integrate with GraphQL resolvers for API endpoints.
|
|
4
|
+
|
|
5
|
+
## 🎯 What is a Service?
|
|
6
|
+
|
|
7
|
+
A Service is a class that contains your application's business logic. Services extend `BaseService` and work with the service registry for dependency management.
|
|
8
|
+
|
|
9
|
+
### Key Features
|
|
10
|
+
|
|
11
|
+
- **Business Logic Organization**: Clean separation of concerns
|
|
12
|
+
- **Type Safety**: Full TypeScript integration with compile-time guarantees
|
|
13
|
+
- **Dependency Injection**: Built-in service registry and dependency management
|
|
14
|
+
- **GraphQL Integration**: Can be used with GraphQL resolvers
|
|
15
|
+
- **Validation**: Input validation and error handling
|
|
16
|
+
|
|
17
|
+
## 🏗️ Creating Services
|
|
18
|
+
|
|
19
|
+
### Basic Service Structure
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { BaseService, ServiceRegistry } from 'bunsane';
|
|
23
|
+
|
|
24
|
+
export default class UserService extends BaseService {
|
|
25
|
+
async createUser(userData: { name: string; email: string; username: string }) {
|
|
26
|
+
const userEntity = UserArcheType.fill(userData).createEntity();
|
|
27
|
+
await userEntity.save();
|
|
28
|
+
return await UserArcheType.Unwrap(userEntity);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getUser(args: { id: string }) {
|
|
32
|
+
const entity = await Entity.FindById(args.id);
|
|
33
|
+
if (!entity) return null;
|
|
34
|
+
return await UserArcheType.Unwrap(entity);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async updateUser(args: any) {
|
|
38
|
+
const entity = await Entity.FindById(args.id);
|
|
39
|
+
if (!entity) throw new Error('User not found');
|
|
40
|
+
|
|
41
|
+
await UserArcheType.updateEntity(entity, args);
|
|
42
|
+
await entity.save();
|
|
43
|
+
return await UserArcheType.Unwrap(entity);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Services are automatically registered when imported
|
|
48
|
+
// No manual registration needed
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### GraphQL Service with Decorators
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { GraphQLObjectType, GraphQLOperation, GraphQLField, GraphQLFieldTypes } from 'bunsane';
|
|
55
|
+
|
|
56
|
+
const userFields = {
|
|
57
|
+
id: GraphQLFieldTypes.ID_REQUIRED,
|
|
58
|
+
name: GraphQLFieldTypes.STRING_OPTIONAL,
|
|
59
|
+
email: GraphQLFieldTypes.STRING_REQUIRED,
|
|
60
|
+
username: GraphQLFieldTypes.STRING_OPTIONAL
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const userInputs = {
|
|
64
|
+
createUser: {
|
|
65
|
+
name: GraphQLFieldTypes.STRING_REQUIRED,
|
|
66
|
+
email: GraphQLFieldTypes.STRING_REQUIRED,
|
|
67
|
+
username: GraphQLFieldTypes.STRING_REQUIRED
|
|
68
|
+
},
|
|
69
|
+
getUser: {
|
|
70
|
+
id: GraphQLFieldTypes.ID_REQUIRED
|
|
71
|
+
},
|
|
72
|
+
updateUser: {
|
|
73
|
+
id: GraphQLFieldTypes.ID_REQUIRED,
|
|
74
|
+
name: GraphQLFieldTypes.STRING_OPTIONAL,
|
|
75
|
+
email: GraphQLFieldTypes.STRING_OPTIONAL
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
@GraphQLObjectType({
|
|
80
|
+
name: "User",
|
|
81
|
+
fields: userFields
|
|
82
|
+
})
|
|
83
|
+
export default class UserService extends BaseService {
|
|
84
|
+
@GraphQLOperation({
|
|
85
|
+
type: "Mutation",
|
|
86
|
+
input: userInputs.createUser,
|
|
87
|
+
output: "User"
|
|
88
|
+
})
|
|
89
|
+
async createUser(args: { name: string; email: string; username: string }) {
|
|
90
|
+
const userEntity = UserArcheType.fill(args).createEntity();
|
|
91
|
+
await userEntity.save();
|
|
92
|
+
return await UserArcheType.Unwrap(userEntity);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@GraphQLOperation({
|
|
96
|
+
type: "Query",
|
|
97
|
+
input: userInputs.getUser,
|
|
98
|
+
output: "User"
|
|
99
|
+
})
|
|
100
|
+
async getUser(args: { id: string }) {
|
|
101
|
+
const entity = await Entity.FindById(args.id);
|
|
102
|
+
if (!entity) return null;
|
|
103
|
+
return await UserArcheType.Unwrap(entity);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@GraphQLOperation({
|
|
107
|
+
type: "Mutation",
|
|
108
|
+
input: userInputs.updateUser,
|
|
109
|
+
output: "User"
|
|
110
|
+
})
|
|
111
|
+
async updateUser(args: { id: string; name?: string; email?: string }) {
|
|
112
|
+
const entity = await Entity.FindById(args.id);
|
|
113
|
+
if (!entity) throw new Error('User not found');
|
|
114
|
+
|
|
115
|
+
await UserArcheType.updateEntity(entity, args);
|
|
116
|
+
await entity.save();
|
|
117
|
+
return await UserArcheType.Unwrap(entity);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@GraphQLField({ type: "User", field: "id" })
|
|
121
|
+
idResolver(parent: Entity) {
|
|
122
|
+
return parent.id;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@GraphQLField({ type: "User", field: "name" })
|
|
126
|
+
async nameResolver(parent: Entity) {
|
|
127
|
+
const profile = await parent.get(UserProfile);
|
|
128
|
+
return profile?.name ?? "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@GraphQLField({ type: "User", field: "email" })
|
|
132
|
+
async emailResolver(parent: Entity) {
|
|
133
|
+
const profile = await parent.get(UserProfile);
|
|
134
|
+
return profile?.email ?? "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 📊 GraphQL Type Definitions
|
|
140
|
+
|
|
141
|
+
### Field Types
|
|
142
|
+
|
|
143
|
+
BunSane provides predefined GraphQL field types for common use cases:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { GraphQLFieldTypes } from 'bunsane';
|
|
147
|
+
|
|
148
|
+
// Available field types
|
|
149
|
+
const fieldTypes = {
|
|
150
|
+
// ID fields
|
|
151
|
+
ID_REQUIRED: GraphQLFieldTypes.ID_REQUIRED, // ID!
|
|
152
|
+
ID_OPTIONAL: GraphQLFieldTypes.ID_OPTIONAL, // ID
|
|
153
|
+
|
|
154
|
+
// String fields
|
|
155
|
+
STRING_REQUIRED: GraphQLFieldTypes.STRING_REQUIRED, // String!
|
|
156
|
+
STRING_OPTIONAL: GraphQLFieldTypes.STRING_OPTIONAL, // String
|
|
157
|
+
|
|
158
|
+
// Numeric fields
|
|
159
|
+
INT_REQUIRED: GraphQLFieldTypes.INT_REQUIRED, // Int!
|
|
160
|
+
INT_OPTIONAL: GraphQLFieldTypes.INT_OPTIONAL, // Int
|
|
161
|
+
FLOAT_REQUIRED: GraphQLFieldTypes.FLOAT_REQUIRED, // Float!
|
|
162
|
+
FLOAT_OPTIONAL: GraphQLFieldTypes.FLOAT_OPTIONAL, // Float
|
|
163
|
+
|
|
164
|
+
// Boolean fields
|
|
165
|
+
BOOLEAN_REQUIRED: GraphQLFieldTypes.BOOLEAN_REQUIRED, // Boolean!
|
|
166
|
+
BOOLEAN_OPTIONAL: GraphQLFieldTypes.BOOLEAN_OPTIONAL, // Boolean
|
|
167
|
+
|
|
168
|
+
// Custom types
|
|
169
|
+
JSON: GraphQLFieldTypes.JSON, // JSON (custom scalar)
|
|
170
|
+
DATE: GraphQLFieldTypes.DATE, // Date (custom scalar)
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Complex Type Definitions
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
export default class BlogService extends BaseService {
|
|
178
|
+
// Post type definition
|
|
179
|
+
postFields = {
|
|
180
|
+
id: GraphQLFieldTypes.ID_REQUIRED,
|
|
181
|
+
title: GraphQLFieldTypes.STRING_REQUIRED,
|
|
182
|
+
content: GraphQLFieldTypes.STRING_REQUIRED,
|
|
183
|
+
author: 'User', // Reference to another type
|
|
184
|
+
tags: '[String]', // Array of strings
|
|
185
|
+
publishedAt: GraphQLFieldTypes.DATE,
|
|
186
|
+
stats: 'PostStats' // Nested object
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Stats nested type
|
|
190
|
+
postStatsFields = {
|
|
191
|
+
viewCount: GraphQLFieldTypes.INT_REQUIRED,
|
|
192
|
+
likeCount: GraphQLFieldTypes.INT_REQUIRED,
|
|
193
|
+
commentCount: GraphQLFieldTypes.INT_REQUIRED
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Input definitions
|
|
197
|
+
postInputs = {
|
|
198
|
+
createPost: {
|
|
199
|
+
title: GraphQLFieldTypes.STRING_REQUIRED,
|
|
200
|
+
content: GraphQLFieldTypes.STRING_REQUIRED,
|
|
201
|
+
tags: '[String]'
|
|
202
|
+
},
|
|
203
|
+
updatePost: {
|
|
204
|
+
id: GraphQLFieldTypes.ID_REQUIRED,
|
|
205
|
+
title: GraphQLFieldTypes.STRING_OPTIONAL,
|
|
206
|
+
content: GraphQLFieldTypes.STRING_OPTIONAL,
|
|
207
|
+
tags: '[String]'
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 🔧 GraphQL Resolvers
|
|
214
|
+
|
|
215
|
+
### Query Resolvers
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
export default class UserService extends BaseService {
|
|
219
|
+
// Simple queries
|
|
220
|
+
async getUser(args: { id: string }) {
|
|
221
|
+
const entity = await Entity.FindById(args.id);
|
|
222
|
+
if (!entity) return null;
|
|
223
|
+
return await UserArcheType.Unwrap(entity);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getUsers(args: { limit?: number; offset?: number }) {
|
|
227
|
+
const query = new Query()
|
|
228
|
+
.with(UserProfile)
|
|
229
|
+
.limit(args.limit || 10)
|
|
230
|
+
.offset(args.offset || 0);
|
|
231
|
+
|
|
232
|
+
const entities = await query.exec();
|
|
233
|
+
return await Promise.all(
|
|
234
|
+
entities.map(entity => UserArcheType.Unwrap(entity))
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Complex queries with filtering
|
|
239
|
+
async searchUsers(args: { query: string; role?: string }) {
|
|
240
|
+
const searchQuery = new Query().with(UserProfile);
|
|
241
|
+
|
|
242
|
+
if (args.role) {
|
|
243
|
+
searchQuery.with(UserRole).filter('role', args.role);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Add text search if supported
|
|
247
|
+
if (args.query) {
|
|
248
|
+
searchQuery.filter('name', `%${args.query}%`, 'LIKE');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const entities = await searchQuery.exec();
|
|
252
|
+
return await Promise.all(
|
|
253
|
+
entities.map(entity => UserArcheType.Unwrap(entity))
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Mutation Resolvers
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
export default class UserService extends BaseService {
|
|
263
|
+
async createUser(args: { input: any }) {
|
|
264
|
+
// Validate input
|
|
265
|
+
if (!args.input.email || !args.input.name) {
|
|
266
|
+
throw new Error('Name and email are required');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check for existing user
|
|
270
|
+
const existingQuery = new Query()
|
|
271
|
+
.with(UserProfile)
|
|
272
|
+
.filter('email', args.input.email);
|
|
273
|
+
|
|
274
|
+
const existing = await existingQuery.exec();
|
|
275
|
+
if (existing.length > 0) {
|
|
276
|
+
throw new Error('User with this email already exists');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create new user
|
|
280
|
+
const userEntity = UserArcheType.fill({
|
|
281
|
+
userProfile: args.input,
|
|
282
|
+
userPreferences: { theme: 'light', notifications: true },
|
|
283
|
+
userStats: { loginCount: 0, lastLogin: new Date() }
|
|
284
|
+
}).createEntity();
|
|
285
|
+
|
|
286
|
+
await userEntity.save();
|
|
287
|
+
return await UserArcheType.Unwrap(userEntity);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async updateUser(args: { id: string; input: any }) {
|
|
291
|
+
const entity = await Entity.FindById(args.id);
|
|
292
|
+
if (!entity) {
|
|
293
|
+
throw new Error('User not found');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Update only provided fields
|
|
297
|
+
const updates: any = {};
|
|
298
|
+
if (args.input.name) updates.userProfile = { name: args.input.name };
|
|
299
|
+
if (args.input.email) updates.userProfile = { ...updates.userProfile, email: args.input.email };
|
|
300
|
+
|
|
301
|
+
await UserArcheType.updateEntity(entity, updates);
|
|
302
|
+
await entity.save();
|
|
303
|
+
|
|
304
|
+
return await UserArcheType.Unwrap(entity);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async deleteUser(args: { id: string }) {
|
|
308
|
+
const entity = await Entity.FindById(args.id);
|
|
309
|
+
if (!entity) {
|
|
310
|
+
throw new Error('User not found');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await entity.delete(true); // Force delete
|
|
314
|
+
return { success: true, message: 'User deleted successfully' };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## 🔗 Service Relationships
|
|
320
|
+
|
|
321
|
+
### Service Dependencies
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
export default class PostService extends BaseService {
|
|
325
|
+
private userService: UserService;
|
|
326
|
+
|
|
327
|
+
async initialize(): Promise<void> {
|
|
328
|
+
await super.initialize();
|
|
329
|
+
// Get service instance from registry
|
|
330
|
+
this.userService = ServiceRegistry.get(UserService);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async createPost(args: { input: any }) {
|
|
334
|
+
// Verify author exists
|
|
335
|
+
const author = await this.userService.getUser({ id: args.input.authorId });
|
|
336
|
+
if (!author) {
|
|
337
|
+
throw new Error('Author not found');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const postEntity = BlogPostArcheType.fill(args.input).createEntity();
|
|
341
|
+
await postEntity.save();
|
|
342
|
+
|
|
343
|
+
return await BlogPostArcheType.Unwrap(postEntity);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async getPostWithAuthor(args: { id: string }) {
|
|
347
|
+
const postEntity = await Entity.FindById(args.id);
|
|
348
|
+
if (!postEntity) return null;
|
|
349
|
+
|
|
350
|
+
const post = await BlogPostArcheType.Unwrap(postEntity);
|
|
351
|
+
|
|
352
|
+
// Fetch author details
|
|
353
|
+
if (post.authorId) {
|
|
354
|
+
post.author = await this.userService.getUser({ id: post.authorId });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return post;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Cross-Service Queries
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
export default class AnalyticsService extends BaseService {
|
|
366
|
+
private userService: UserService;
|
|
367
|
+
private postService: PostService;
|
|
368
|
+
|
|
369
|
+
async initialize(): Promise<void> {
|
|
370
|
+
await super.initialize();
|
|
371
|
+
this.userService = ServiceRegistry.get(UserService);
|
|
372
|
+
this.postService = ServiceRegistry.get(PostService);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async getUserStats(args: { userId: string }) {
|
|
376
|
+
const user = await this.userService.getUser({ id: args.userId });
|
|
377
|
+
if (!user) return null;
|
|
378
|
+
|
|
379
|
+
// Get user's posts
|
|
380
|
+
const userPosts = await new Query()
|
|
381
|
+
.with(BlogPost)
|
|
382
|
+
.filter('authorId', args.userId)
|
|
383
|
+
.exec();
|
|
384
|
+
|
|
385
|
+
// Calculate stats
|
|
386
|
+
const totalPosts = userPosts.length;
|
|
387
|
+
const totalViews = userPosts.reduce((sum, post) => {
|
|
388
|
+
const stats = post.get(PostStats);
|
|
389
|
+
return sum + (stats?.viewCount || 0);
|
|
390
|
+
}, 0);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
user: user,
|
|
394
|
+
stats: {
|
|
395
|
+
totalPosts,
|
|
396
|
+
totalViews,
|
|
397
|
+
averageViews: totalPosts > 0 ? totalViews / totalPosts : 0
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## 🎭 Advanced Service Patterns
|
|
405
|
+
|
|
406
|
+
### Service with Middleware
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
export default class SecureService extends BaseService {
|
|
410
|
+
// Middleware for authentication
|
|
411
|
+
async authenticate(context: any, next: Function) {
|
|
412
|
+
const token = context.request.headers.authorization;
|
|
413
|
+
if (!token) {
|
|
414
|
+
throw new Error('Authentication required');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Verify token and set user context
|
|
418
|
+
const user = await this.verifyToken(token);
|
|
419
|
+
context.user = user;
|
|
420
|
+
|
|
421
|
+
return next();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Middleware for authorization
|
|
425
|
+
async authorize(context: any, next: Function) {
|
|
426
|
+
if (!context.user.isAdmin) {
|
|
427
|
+
throw new Error('Admin access required');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return next();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Protected resolver
|
|
434
|
+
async adminOnlyAction(args: any, context: any) {
|
|
435
|
+
// This resolver is automatically protected by middleware
|
|
436
|
+
return { success: true, user: context.user };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Service with Caching
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
export default class CachedUserService extends BaseService {
|
|
445
|
+
private cache = new Map<string, any>();
|
|
446
|
+
|
|
447
|
+
async getUser(args: { id: string }) {
|
|
448
|
+
// Check cache first
|
|
449
|
+
const cacheKey = `user:${args.id}`;
|
|
450
|
+
if (this.cache.has(cacheKey)) {
|
|
451
|
+
return this.cache.get(cacheKey);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Fetch from database
|
|
455
|
+
const entity = await Entity.FindById(args.id);
|
|
456
|
+
if (!entity) return null;
|
|
457
|
+
|
|
458
|
+
const user = await UserArcheType.Unwrap(entity);
|
|
459
|
+
|
|
460
|
+
// Cache for 5 minutes
|
|
461
|
+
this.cache.set(cacheKey, user);
|
|
462
|
+
setTimeout(() => {
|
|
463
|
+
this.cache.delete(cacheKey);
|
|
464
|
+
}, 5 * 60 * 1000);
|
|
465
|
+
|
|
466
|
+
return user;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Invalidate cache on updates
|
|
470
|
+
async updateUser(args: any) {
|
|
471
|
+
const result = await super.updateUser(args);
|
|
472
|
+
|
|
473
|
+
// Clear cache
|
|
474
|
+
const cacheKey = `user:${args.id}`;
|
|
475
|
+
this.cache.delete(cacheKey);
|
|
476
|
+
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Batch Operations Service
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
export default class BatchService extends BaseService {
|
|
486
|
+
async createUsers(args: { inputs: any[] }) {
|
|
487
|
+
const results = [];
|
|
488
|
+
const errors = [];
|
|
489
|
+
|
|
490
|
+
for (const input of args.inputs) {
|
|
491
|
+
try {
|
|
492
|
+
const user = await this.createUser({ input });
|
|
493
|
+
results.push(user);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
errors.push({
|
|
496
|
+
input,
|
|
497
|
+
error: error.message
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
results,
|
|
504
|
+
errors,
|
|
505
|
+
success: errors.length === 0
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async bulkUpdateUsers(args: { updates: Array<{ id: string; input: any }> }) {
|
|
510
|
+
const results = [];
|
|
511
|
+
const errors = [];
|
|
512
|
+
|
|
513
|
+
// Process in batches to avoid overwhelming the database
|
|
514
|
+
const batchSize = 10;
|
|
515
|
+
for (let i = 0; i < args.updates.length; i += batchSize) {
|
|
516
|
+
const batch = args.updates.slice(i, i + batchSize);
|
|
517
|
+
|
|
518
|
+
const batchPromises = batch.map(async (update) => {
|
|
519
|
+
try {
|
|
520
|
+
const user = await this.updateUser({
|
|
521
|
+
id: update.id,
|
|
522
|
+
input: update.input
|
|
523
|
+
});
|
|
524
|
+
return { success: true, user };
|
|
525
|
+
} catch (error) {
|
|
526
|
+
return { success: false, error: error.message, id: update.id };
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const batchResults = await Promise.all(batchPromises);
|
|
531
|
+
results.push(...batchResults);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
results,
|
|
536
|
+
errors: results.filter(r => !r.success),
|
|
537
|
+
success: results.every(r => r.success)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## 🔧 Service Registry
|
|
544
|
+
|
|
545
|
+
### Automatic Service Registration
|
|
546
|
+
|
|
547
|
+
Services are automatically discovered and registered when:
|
|
548
|
+
1. They extend `BaseService`
|
|
549
|
+
2. They are imported in your application entry point
|
|
550
|
+
3. The application starts via `App.start()`
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
// app.ts
|
|
554
|
+
import { App } from 'bunsane';
|
|
555
|
+
import UserService from './services/UserService';
|
|
556
|
+
import PostService from './services/PostService';
|
|
557
|
+
|
|
558
|
+
const app = new App();
|
|
559
|
+
|
|
560
|
+
// Services are automatically registered when imported
|
|
561
|
+
// No manual registration required
|
|
562
|
+
app.start();
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Accessing Services
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// Get service instance
|
|
569
|
+
const userService = ServiceRegistry.get(UserService);
|
|
570
|
+
const postService = ServiceRegistry.get(PostService);
|
|
571
|
+
|
|
572
|
+
// Use services in your application
|
|
573
|
+
const user = await userService.getUser({ id: '123' });
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## 📊 Best Practices
|
|
577
|
+
|
|
578
|
+
### Service Design
|
|
579
|
+
|
|
580
|
+
- **Single Responsibility**: Each service should handle one domain area
|
|
581
|
+
- **Dependency Injection**: Use constructor injection for dependencies
|
|
582
|
+
- **Error Handling**: Provide clear, actionable error messages
|
|
583
|
+
- **Validation**: Validate inputs before processing
|
|
584
|
+
- **Documentation**: Document complex business logic
|
|
585
|
+
|
|
586
|
+
### Performance Considerations
|
|
587
|
+
|
|
588
|
+
- **Caching**: Implement caching for frequently accessed data
|
|
589
|
+
- **Batch Operations**: Support bulk operations where possible
|
|
590
|
+
- **Lazy Loading**: Only fetch related data when needed
|
|
591
|
+
- **Query Optimization**: Use efficient database queries
|
|
592
|
+
- **Connection Pooling**: Reuse database connections
|
|
593
|
+
|
|
594
|
+
### Error Handling
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
export default class RobustService extends BaseService {
|
|
598
|
+
async safeOperation(args: any) {
|
|
599
|
+
try {
|
|
600
|
+
// Validate input
|
|
601
|
+
this.validateInput(args);
|
|
602
|
+
|
|
603
|
+
// Perform operation
|
|
604
|
+
const result = await this.performOperation(args);
|
|
605
|
+
|
|
606
|
+
// Log success
|
|
607
|
+
logger.info('Operation completed successfully', { args, result });
|
|
608
|
+
|
|
609
|
+
return result;
|
|
610
|
+
|
|
611
|
+
} catch (error) {
|
|
612
|
+
// Log error with context
|
|
613
|
+
logger.error('Operation failed', {
|
|
614
|
+
args,
|
|
615
|
+
error: error.message,
|
|
616
|
+
stack: error.stack
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Return user-friendly error
|
|
620
|
+
throw new Error('Operation failed. Please try again later.');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private validateInput(args: any) {
|
|
625
|
+
if (!args.id) {
|
|
626
|
+
throw new Error('ID is required');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (args.value && args.value < 0) {
|
|
630
|
+
throw new Error('Value must be positive');
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## 🚀 What's Next?
|
|
637
|
+
|
|
638
|
+
Now that you understand Services, let's explore:
|
|
639
|
+
|
|
640
|
+
- **[Query System](query.md)** - Efficient data retrieval
|
|
641
|
+
- **[Lifecycle Hooks](hooks.md)** - Business logic integration
|
|
642
|
+
- **[Entity System](entity.md)** - How entities work with services
|
|
643
|
+
- **[Advanced Features](../advanced/)** - Power user capabilities
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
*Ready to build APIs with services? Let's look at the [Query System](query.md) next!* 🚀
|