fragment-ts 1.0.17 → 1.0.18

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.
Files changed (33) hide show
  1. package/SETUP.md +570 -0
  2. package/changes/1.md +420 -0
  3. package/dist/cli/commands/init.command.js +4 -4
  4. package/dist/cli/commands/init.command.js.map +1 -1
  5. package/dist/cli/commands/test.command.d.ts +6 -0
  6. package/dist/cli/commands/test.command.d.ts.map +1 -0
  7. package/dist/cli/commands/test.command.js +311 -0
  8. package/dist/cli/commands/test.command.js.map +1 -0
  9. package/dist/cli/index.js +6 -3
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/core/container/di-container.d.ts +5 -0
  12. package/dist/core/container/di-container.d.ts.map +1 -1
  13. package/dist/core/container/di-container.js +121 -21
  14. package/dist/core/container/di-container.js.map +1 -1
  15. package/dist/core/decorators/controller.decorator.js +5 -5
  16. package/dist/core/decorators/injection.decorators.d.ts +44 -1
  17. package/dist/core/decorators/injection.decorators.d.ts.map +1 -1
  18. package/dist/core/decorators/injection.decorators.js +92 -1
  19. package/dist/core/decorators/injection.decorators.js.map +1 -1
  20. package/dist/core/metadata/metadata-keys.d.ts +29 -17
  21. package/dist/core/metadata/metadata-keys.d.ts.map +1 -1
  22. package/dist/core/metadata/metadata-keys.js +35 -17
  23. package/dist/core/metadata/metadata-keys.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/cli/commands/init.command.ts +4 -4
  26. package/src/cli/commands/test.command.ts +289 -0
  27. package/src/cli/index.ts +16 -11
  28. package/src/core/container/di-container.ts +166 -31
  29. package/src/core/decorators/DECORATOR_USAGE.md +326 -0
  30. package/src/core/decorators/controller.decorator.ts +9 -9
  31. package/src/core/decorators/injection.decorators.ts +129 -5
  32. package/src/core/metadata/metadata-keys.ts +44 -18
  33. package/src/testing/TEST.md +321 -0
@@ -1,11 +1,12 @@
1
- import { METADATA_KEYS } from '../metadata/metadata-keys';
2
- import { Scope } from '../decorators/injectable.decorator';
1
+ import { METADATA_KEYS } from "../metadata/metadata-keys";
2
+ import { Scope } from "../decorators/injectable.decorator";
3
3
 
4
4
  export class DIContainer {
5
5
  private static instance: DIContainer;
6
6
  private singletons: Map<any, any> = new Map();
7
7
  private transients: Map<any, any> = new Map();
8
8
  private factories: Map<any, () => any> = new Map();
9
+ private constructing: Set<any> = new Set(); // Prevent circular dependencies
9
10
 
10
11
  static getInstance(): DIContainer {
11
12
  if (!DIContainer.instance) {
@@ -23,78 +24,212 @@ export class DIContainer {
23
24
  }
24
25
 
25
26
  resolve<T>(token: any): T {
27
+ // Check for circular dependency
28
+ if (this.constructing.has(token)) {
29
+ throw new Error(
30
+ `Circular dependency detected for ${token.name || token}`,
31
+ );
32
+ }
33
+
34
+ // Return existing singleton
26
35
  if (this.singletons.has(token)) {
27
36
  return this.singletons.get(token);
28
37
  }
29
38
 
39
+ // Use factory if available
30
40
  if (this.factories.has(token)) {
31
41
  const factory = this.factories.get(token)!;
32
42
  const instance = factory();
33
- const scope = Reflect.getMetadata(METADATA_KEYS.SCOPE, token) || 'singleton';
34
-
35
- if (scope === 'singleton') {
43
+ const scope =
44
+ Reflect.getMetadata(METADATA_KEYS.SCOPE, token) || "singleton";
45
+
46
+ if (scope === "singleton") {
36
47
  this.singletons.set(token, instance);
37
48
  }
38
-
49
+
39
50
  return instance;
40
51
  }
41
52
 
42
- if (typeof token === 'function') {
43
- const instance = this.createInstance(token);
44
- const scope = Reflect.getMetadata(METADATA_KEYS.SCOPE, token) || 'singleton';
45
-
46
- if (scope === 'singleton') {
47
- this.singletons.set(token, instance);
53
+ // Create new instance
54
+ if (typeof token === "function") {
55
+ this.constructing.add(token);
56
+
57
+ try {
58
+ const instance = this.createInstance(token);
59
+ const scope =
60
+ Reflect.getMetadata(METADATA_KEYS.SCOPE, token) || "singleton";
61
+
62
+ if (scope === "singleton") {
63
+ this.singletons.set(token, instance);
64
+ }
65
+
66
+ this.constructing.delete(token);
67
+ return instance;
68
+ } catch (error) {
69
+ this.constructing.delete(token);
70
+ throw error;
48
71
  }
49
-
50
- return instance;
51
72
  }
52
73
 
53
74
  throw new Error(`Cannot resolve dependency: ${token}`);
54
75
  }
55
76
 
56
77
  private createInstance(target: any): any {
57
- const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
58
- const params = paramTypes.map((type: any) => this.resolve(type));
78
+ // Get constructor parameter types
79
+ const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
80
+
81
+ // Resolve constructor dependencies
82
+ const params = paramTypes.map((type: any) => {
83
+ if (!type || type === Object) {
84
+ return undefined;
85
+ }
86
+ return this.resolve(type);
87
+ });
88
+
89
+ // Create instance
59
90
  const instance = new target(...params);
60
-
91
+
92
+ // Inject properties AFTER construction
61
93
  this.injectProperties(instance);
62
-
94
+
95
+ // Call post-construct hook if it exists
96
+ if (typeof instance.onInit === "function") {
97
+ instance.onInit();
98
+ }
99
+
63
100
  return instance;
64
101
  }
65
102
 
66
103
  private injectProperties(instance: any): void {
67
- const properties = Object.getOwnPropertyNames(instance.constructor.prototype);
68
-
69
- properties.forEach(prop => {
70
- const autowired = Reflect.getMetadata(METADATA_KEYS.AUTOWIRED, instance, prop);
71
- const inject = Reflect.getMetadata(METADATA_KEYS.INJECT, instance, prop);
72
- const value = Reflect.getMetadata(METADATA_KEYS.VALUE, instance, prop);
73
-
104
+ const prototype = Object.getPrototypeOf(instance);
105
+
106
+ // Get all property keys from the prototype chain
107
+ const properties = this.getAllPropertyKeys(instance);
108
+
109
+ properties.forEach((prop) => {
110
+ // Check for @Autowired
111
+ const autowired = Reflect.getMetadata(
112
+ METADATA_KEYS.AUTOWIRED,
113
+ prototype,
114
+ prop,
115
+ );
74
116
  if (autowired) {
75
117
  instance[prop] = this.resolve(autowired);
76
- } else if (inject) {
118
+ return;
119
+ }
120
+
121
+ // Check for @Inject
122
+ const inject = Reflect.getMetadata(METADATA_KEYS.INJECT, prototype, prop);
123
+ if (inject) {
77
124
  instance[prop] = this.resolve(inject);
78
- } else if (value) {
125
+ return;
126
+ }
127
+
128
+ // Check for @Value
129
+ const value = Reflect.getMetadata(METADATA_KEYS.VALUE, prototype, prop);
130
+ if (value !== undefined) {
79
131
  instance[prop] = this.resolveValue(value);
132
+ return;
133
+ }
134
+
135
+ // Check for @InjectRepository - special TypeORM handling
136
+ const repositoryEntity = Reflect.getMetadata(
137
+ METADATA_KEYS.INJECT_REPOSITORY,
138
+ prototype,
139
+ prop,
140
+ );
141
+ if (repositoryEntity) {
142
+ instance[prop] = this.resolveRepository(repositoryEntity);
143
+ return;
80
144
  }
81
145
  });
82
146
  }
83
147
 
148
+ private getAllPropertyKeys(obj: any): string[] {
149
+ const keys = new Set<string>();
150
+ let current = obj;
151
+
152
+ while (current && current !== Object.prototype) {
153
+ Object.getOwnPropertyNames(current).forEach((key) => keys.add(key));
154
+ current = Object.getPrototypeOf(current);
155
+ }
156
+
157
+ return Array.from(keys);
158
+ }
159
+
84
160
  private resolveValue(expression: string): any {
85
- const match = expression.match(/\$\{(.+?)\}/);
161
+ // Handle environment variable syntax: ${VAR_NAME} or $VAR_NAME
162
+ const match = expression.match(/\$\{([^}]+)\}|\$([A-Z_][A-Z0-9_]*)/i);
86
163
  if (match) {
87
- const key = match[1];
88
- return process.env[key] || '';
164
+ const key = match[1] || match[2];
165
+ const value = process.env[key];
166
+
167
+ if (value === undefined) {
168
+ console.warn(
169
+ `⚠️ Warning: Environment variable "${key}" is not defined`,
170
+ );
171
+ return "";
172
+ }
173
+
174
+ // Try to parse as JSON for complex types
175
+ if (value.startsWith("{") || value.startsWith("[")) {
176
+ try {
177
+ return JSON.parse(value);
178
+ } catch {
179
+ return value;
180
+ }
181
+ }
182
+
183
+ // Parse numbers
184
+ if (/^\d+$/.test(value)) {
185
+ return parseInt(value, 10);
186
+ }
187
+
188
+ // Parse booleans
189
+ if (value.toLowerCase() === "true") return true;
190
+ if (value.toLowerCase() === "false") return false;
191
+
192
+ return value;
89
193
  }
194
+
90
195
  return expression;
91
196
  }
92
197
 
198
+ private resolveRepository(entity: any): any {
199
+ try {
200
+ // Import TypeORM module dynamically to avoid circular dependencies
201
+ const { TypeORMModule } = require("../../typeorm/typeorm-module");
202
+ const dataSource = TypeORMModule.getDataSource();
203
+ return dataSource.getRepository(entity);
204
+ } catch (error) {
205
+ throw new Error(
206
+ `Failed to resolve repository for entity ${entity.name}: ${(error as Error)?.message}. ` +
207
+ `Make sure TypeORM is initialized before using @InjectRepository.`,
208
+ );
209
+ }
210
+ }
211
+
93
212
  has(token: any): boolean {
94
- return this.singletons.has(token) || this.factories.has(token);
213
+ return (
214
+ this.singletons.has(token) ||
215
+ this.factories.has(token) ||
216
+ typeof token === "function"
217
+ );
95
218
  }
96
219
 
97
220
  getAllInstances(): any[] {
98
221
  return Array.from(this.singletons.values());
99
222
  }
223
+
224
+ clear(): void {
225
+ this.singletons.clear();
226
+ this.transients.clear();
227
+ this.factories.clear();
228
+ this.constructing.clear();
229
+ }
230
+
231
+ // For testing purposes
232
+ reset(): void {
233
+ this.clear();
234
+ }
100
235
  }
@@ -0,0 +1,326 @@
1
+ // ============================================
2
+ // Example: Complete Fragment Application
3
+ // Demonstrates all decorators working together
4
+ // ============================================
5
+
6
+ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
7
+ import {
8
+ FragmentApplication,
9
+ Controller,
10
+ Service,
11
+ Repository,
12
+ Injectable,
13
+ Get,
14
+ Post,
15
+ Body,
16
+ Param,
17
+ Autowired,
18
+ InjectRepository,
19
+ Value,
20
+ PostConstruct,
21
+ } from 'fragment-ts';
22
+ import { Repository as TypeORMRepository } from 'typeorm';
23
+
24
+ // ============================================
25
+ // 1. Entity Definition
26
+ // ============================================
27
+ @Entity()
28
+ export class User {
29
+ @PrimaryGeneratedColumn()
30
+ id!: number;
31
+
32
+ @Column()
33
+ name!: string;
34
+
35
+ @Column()
36
+ email!: string;
37
+
38
+ @Column({ default: true })
39
+ isActive!: boolean;
40
+ }
41
+
42
+ // ============================================
43
+ // 2. Repository Layer
44
+ // ============================================
45
+ @Repository()
46
+ export class UserRepository {
47
+ // TypeORM repository is automatically injected
48
+ @InjectRepository(User)
49
+ private repo!: TypeORMRepository<User>;
50
+
51
+ async findAll(): Promise<User[]> {
52
+ return this.repo.find();
53
+ }
54
+
55
+ async findById(id: number): Promise<User | null> {
56
+ return this.repo.findOne({ where: { id } });
57
+ }
58
+
59
+ async create(data: Partial<User>): Promise<User> {
60
+ const user = this.repo.create(data);
61
+ return this.repo.save(user);
62
+ }
63
+
64
+ async update(id: number, data: Partial<User>): Promise<User | null> {
65
+ await this.repo.update(id, data);
66
+ return this.findById(id);
67
+ }
68
+
69
+ async delete(id: number): Promise<boolean> {
70
+ const result = await this.repo.delete(id);
71
+ return (result.affected ?? 0) > 0;
72
+ }
73
+
74
+ async findByEmail(email: string): Promise<User | null> {
75
+ return this.repo.findOne({ where: { email } });
76
+ }
77
+ }
78
+
79
+ // ============================================
80
+ // 3. Service Layer with Configuration
81
+ // ============================================
82
+ @Injectable()
83
+ export class EmailService {
84
+ @Value('${EMAIL_FROM}')
85
+ private fromEmail!: string;
86
+
87
+ @Value('${EMAIL_ENABLED:true}')
88
+ private enabled!: boolean;
89
+
90
+ @PostConstruct()
91
+ init() {
92
+ console.log(`📧 Email service initialized`);
93
+ console.log(` From: ${this.fromEmail}`);
94
+ console.log(` Enabled: ${this.enabled}`);
95
+ }
96
+
97
+ async sendWelcomeEmail(email: string, name: string): Promise<void> {
98
+ if (!this.enabled) {
99
+ console.log(`Email disabled, skipping welcome email to ${email}`);
100
+ return;
101
+ }
102
+
103
+ console.log(`Sending welcome email to ${email}`);
104
+ // Actual email sending logic would go here
105
+ }
106
+
107
+ async sendPasswordReset(email: string, token: string): Promise<void> {
108
+ if (!this.enabled) {
109
+ console.log(`Email disabled, skipping reset email to ${email}`);
110
+ return;
111
+ }
112
+
113
+ console.log(`Sending password reset to ${email} with token ${token}`);
114
+ }
115
+ }
116
+
117
+ // ============================================
118
+ // 4. Business Logic Service
119
+ // ============================================
120
+ @Service()
121
+ export class UserService {
122
+ // Dependencies are automatically injected using @Autowired
123
+ @Autowired()
124
+ private userRepository!: UserRepository;
125
+
126
+ @Autowired()
127
+ private emailService!: EmailService;
128
+
129
+ @Value('${MAX_USERS:1000}')
130
+ private maxUsers!: number;
131
+
132
+ @PostConstruct()
133
+ init() {
134
+ console.log(`👤 User service initialized (max users: ${this.maxUsers})`);
135
+ }
136
+
137
+ async getAllUsers(): Promise<User[]> {
138
+ return this.userRepository.findAll();
139
+ }
140
+
141
+ async getUserById(id: number): Promise<User | null> {
142
+ return this.userRepository.findById(id);
143
+ }
144
+
145
+ async createUser(data: Partial<User>): Promise<User> {
146
+ // Check max users limit
147
+ const users = await this.userRepository.findAll();
148
+ if (users.length >= this.maxUsers) {
149
+ throw new Error(`Maximum user limit (${this.maxUsers}) reached`);
150
+ }
151
+
152
+ // Check if email already exists
153
+ if (data.email) {
154
+ const existing = await this.userRepository.findByEmail(data.email);
155
+ if (existing) {
156
+ throw new Error(`User with email ${data.email} already exists`);
157
+ }
158
+ }
159
+
160
+ // Create user
161
+ const user = await this.userRepository.create(data);
162
+
163
+ // Send welcome email
164
+ if (user.email && user.name) {
165
+ await this.emailService.sendWelcomeEmail(user.email, user.name);
166
+ }
167
+
168
+ return user;
169
+ }
170
+
171
+ async updateUser(id: number, data: Partial<User>): Promise<User | null> {
172
+ const user = await this.userRepository.findById(id);
173
+ if (!user) {
174
+ throw new Error(`User with id ${id} not found`);
175
+ }
176
+
177
+ return this.userRepository.update(id, data);
178
+ }
179
+
180
+ async deleteUser(id: number): Promise<boolean> {
181
+ const user = await this.userRepository.findById(id);
182
+ if (!user) {
183
+ throw new Error(`User with id ${id} not found`);
184
+ }
185
+
186
+ return this.userRepository.delete(id);
187
+ }
188
+
189
+ async requestPasswordReset(email: string): Promise<void> {
190
+ const user = await this.userRepository.findByEmail(email);
191
+ if (!user) {
192
+ throw new Error(`User with email ${email} not found`);
193
+ }
194
+
195
+ // Generate reset token (simplified)
196
+ const token = Math.random().toString(36).substring(7);
197
+ await this.emailService.sendPasswordReset(email, token);
198
+ }
199
+ }
200
+
201
+ // ============================================
202
+ // 5. Controller Layer
203
+ // ============================================
204
+ @Controller('/api/users')
205
+ export class UserController {
206
+ // Service is automatically injected
207
+ @Autowired()
208
+ private userService!: UserService;
209
+
210
+ @Get('/')
211
+ async getAllUsers() {
212
+ const users = await this.userService.getAllUsers();
213
+ return {
214
+ success: true,
215
+ data: users,
216
+ count: users.length,
217
+ };
218
+ }
219
+
220
+ @Get('/:id')
221
+ async getUserById(@Param('id') id: string) {
222
+ const userId = parseInt(id, 10);
223
+ const user = await this.userService.getUserById(userId);
224
+
225
+ if (!user) {
226
+ return {
227
+ success: false,
228
+ error: 'User not found',
229
+ };
230
+ }
231
+
232
+ return {
233
+ success: true,
234
+ data: user,
235
+ };
236
+ }
237
+
238
+ @Post('/')
239
+ async createUser(@Body() data: any) {
240
+ try {
241
+ const user = await this.userService.createUser(data);
242
+ return {
243
+ success: true,
244
+ data: user,
245
+ message: 'User created successfully',
246
+ };
247
+ } catch (error: any) {
248
+ return {
249
+ success: false,
250
+ error: error.message,
251
+ };
252
+ }
253
+ }
254
+
255
+ @Post('/:id')
256
+ async updateUser(@Param('id') id: string, @Body() data: any) {
257
+ try {
258
+ const userId = parseInt(id, 10);
259
+ const user = await this.userService.updateUser(userId, data);
260
+ return {
261
+ success: true,
262
+ data: user,
263
+ message: 'User updated successfully',
264
+ };
265
+ } catch (error: any) {
266
+ return {
267
+ success: false,
268
+ error: error.message,
269
+ };
270
+ }
271
+ }
272
+
273
+ @Post('/:id/delete')
274
+ async deleteUser(@Param('id') id: string) {
275
+ try {
276
+ const userId = parseInt(id, 10);
277
+ const deleted = await this.userService.deleteUser(userId);
278
+ return {
279
+ success: deleted,
280
+ message: deleted ? 'User deleted successfully' : 'Failed to delete user',
281
+ };
282
+ } catch (error: any) {
283
+ return {
284
+ success: false,
285
+ error: error.message,
286
+ };
287
+ }
288
+ }
289
+
290
+ @Post('/password-reset')
291
+ async requestPasswordReset(@Body() data: { email: string }) {
292
+ try {
293
+ await this.userService.requestPasswordReset(data.email);
294
+ return {
295
+ success: true,
296
+ message: 'Password reset email sent',
297
+ };
298
+ } catch (error: any) {
299
+ return {
300
+ success: false,
301
+ error: error.message,
302
+ };
303
+ }
304
+ }
305
+ }
306
+
307
+ // ============================================
308
+ // 6. Application Entry Point
309
+ // ============================================
310
+ @FragmentApplication({
311
+ port: 3000,
312
+ host: '0.0.0.0',
313
+ })
314
+ export class App {}
315
+
316
+ // ============================================
317
+ // 7. Bootstrap
318
+ // ============================================
319
+ import { FragmentWebApplication } from 'fragment-ts';
320
+
321
+ async function bootstrap() {
322
+ const app = new FragmentWebApplication();
323
+ await app.bootstrap(App);
324
+ }
325
+
326
+ bootstrap().catch(console.error);
@@ -1,18 +1,18 @@
1
- import { Injectable } from './injectable.decorator';
2
- import { METADATA_KEYS } from '../metadata/metadata-keys';
3
- import { MetadataStorage } from '../metadata/metadata-storage';
1
+ import { Injectable } from "./injectable.decorator";
2
+ import { METADATA_KEYS } from "../metadata/metadata-keys";
3
+ import { MetadataStorage } from "../metadata/metadata-storage";
4
4
 
5
- export function Controller(path: string = ''): ClassDecorator {
5
+ export function Controller(path: string = ""): ClassDecorator {
6
6
  return (target: any) => {
7
- Injectable('singleton')(target);
7
+ Injectable("singleton")(target);
8
8
  Reflect.defineMetadata(METADATA_KEYS.CONTROLLER, path, target);
9
-
9
+
10
10
  const storage = MetadataStorage.getInstance();
11
11
  storage.addClass({
12
12
  target,
13
- type: 'controller',
14
- scope: 'singleton',
15
- path
13
+ type: "controller",
14
+ scope: "singleton",
15
+ path,
16
16
  });
17
17
  };
18
18
  }