@webpieces/dev-config 0.0.0-dev
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 +306 -0
- package/bin/set-version.sh +86 -0
- package/bin/setup-claude-patterns.sh +51 -0
- package/bin/start.sh +107 -0
- package/bin/stop.sh +65 -0
- package/bin/use-local-webpieces.sh +89 -0
- package/bin/use-published-webpieces.sh +33 -0
- package/config/eslint/base.mjs +91 -0
- package/config/typescript/tsconfig.base.json +25 -0
- package/eslint-plugin/__tests__/catch-error-pattern.test.ts +360 -0
- package/eslint-plugin/__tests__/max-file-lines.test.ts +195 -0
- package/eslint-plugin/__tests__/max-method-lines.test.ts +246 -0
- package/eslint-plugin/index.d.ts +14 -0
- package/eslint-plugin/index.js +19 -0
- package/eslint-plugin/index.js.map +1 -0
- package/eslint-plugin/index.ts +18 -0
- package/eslint-plugin/rules/catch-error-pattern.d.ts +11 -0
- package/eslint-plugin/rules/catch-error-pattern.js +196 -0
- package/eslint-plugin/rules/catch-error-pattern.js.map +1 -0
- package/eslint-plugin/rules/catch-error-pattern.ts +281 -0
- package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
- package/eslint-plugin/rules/max-file-lines.js +257 -0
- package/eslint-plugin/rules/max-file-lines.js.map +1 -0
- package/eslint-plugin/rules/max-file-lines.ts +272 -0
- package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
- package/eslint-plugin/rules/max-method-lines.js +257 -0
- package/eslint-plugin/rules/max-method-lines.js.map +1 -0
- package/eslint-plugin/rules/max-method-lines.ts +304 -0
- package/package.json +54 -0
- package/patterns/CLAUDE.md +293 -0
- package/patterns/claude.patterns.md +798 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
# Coding Patterns for webpieces-ts
|
|
2
|
+
|
|
3
|
+
This file contains specific coding patterns and conventions used in the webpieces-ts project. These patterns should be followed consistently when adding new features or modifying existing code.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
1. [Classes vs Interfaces](#classes-vs-interfaces)
|
|
7
|
+
2. [Data Structure Patterns](#data-structure-patterns)
|
|
8
|
+
3. [Filter Chain Patterns](#filter-chain-patterns)
|
|
9
|
+
4. [Dependency Injection Patterns](#dependency-injection-patterns)
|
|
10
|
+
5. [Decorator Patterns](#decorator-patterns)
|
|
11
|
+
6. [Testing Patterns](#testing-patterns)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Classes vs Interfaces
|
|
16
|
+
|
|
17
|
+
### The Golden Rule
|
|
18
|
+
|
|
19
|
+
**DATA ONLY → Class**
|
|
20
|
+
**BUSINESS LOGIC → Interface**
|
|
21
|
+
|
|
22
|
+
### Decision Tree
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Does the type have methods with business logic?
|
|
26
|
+
├─ YES → Use Interface
|
|
27
|
+
│ └─ Examples: Filter, Routes, RouteBuilder, WebAppMeta, SaveApi
|
|
28
|
+
│
|
|
29
|
+
└─ NO → Use Class
|
|
30
|
+
└─ Is it just data/configuration?
|
|
31
|
+
└─ YES → Use Class
|
|
32
|
+
└─ Examples: ClientConfig, FilterDefinition, RouteDefinition,
|
|
33
|
+
MethodMeta, Action, RouteMetadata
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Why This Matters
|
|
37
|
+
|
|
38
|
+
**Classes provide:**
|
|
39
|
+
1. **Explicit construction** - No anonymous object literals
|
|
40
|
+
2. **Validation at creation** - Enforce required fields
|
|
41
|
+
3. **Default values** - Set defaults in constructor
|
|
42
|
+
4. **Type safety** - Clear instantiation points
|
|
43
|
+
5. **Debuggability** - Explicit class names in stack traces
|
|
44
|
+
|
|
45
|
+
**Interfaces provide:**
|
|
46
|
+
1. **Polymorphism** - Multiple implementations
|
|
47
|
+
2. **Abstraction** - Define contracts without implementation
|
|
48
|
+
3. **Dependency Inversion** - Depend on abstractions
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Data Structure Patterns
|
|
53
|
+
|
|
54
|
+
### Pattern 1: Simple Data Class
|
|
55
|
+
|
|
56
|
+
For data with all required fields:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export class SaveRequest {
|
|
60
|
+
query: string;
|
|
61
|
+
meta?: RequestMetadata;
|
|
62
|
+
|
|
63
|
+
constructor(query: string, meta?: RequestMetadata) {
|
|
64
|
+
this.query = query;
|
|
65
|
+
this.meta = meta;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Usage
|
|
70
|
+
const request = new SaveRequest('search term', metadata);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Pattern 2: Configuration Class with Defaults
|
|
74
|
+
|
|
75
|
+
For configuration with optional fields and defaults:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
export class JsonFilterConfig {
|
|
79
|
+
validationEnabled: boolean;
|
|
80
|
+
loggingEnabled: boolean;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
validationEnabled: boolean = true,
|
|
84
|
+
loggingEnabled: boolean = false
|
|
85
|
+
) {
|
|
86
|
+
this.validationEnabled = validationEnabled;
|
|
87
|
+
this.loggingEnabled = loggingEnabled;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Usage
|
|
92
|
+
const config = new JsonFilterConfig(); // Uses defaults
|
|
93
|
+
const customConfig = new JsonFilterConfig(false, true); // Custom values
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Pattern 3: Metadata Class
|
|
97
|
+
|
|
98
|
+
For metadata with many optional fields:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
export class MethodMeta {
|
|
102
|
+
httpMethod: string;
|
|
103
|
+
path: string;
|
|
104
|
+
methodName: string;
|
|
105
|
+
params: any[];
|
|
106
|
+
request?: any;
|
|
107
|
+
response?: any;
|
|
108
|
+
metadata?: Map<string, any>;
|
|
109
|
+
|
|
110
|
+
constructor(
|
|
111
|
+
httpMethod: string,
|
|
112
|
+
path: string,
|
|
113
|
+
methodName: string,
|
|
114
|
+
params: any[],
|
|
115
|
+
request?: any,
|
|
116
|
+
response?: any,
|
|
117
|
+
metadata?: Map<string, any>
|
|
118
|
+
) {
|
|
119
|
+
this.httpMethod = httpMethod;
|
|
120
|
+
this.path = path;
|
|
121
|
+
this.methodName = methodName;
|
|
122
|
+
this.params = params;
|
|
123
|
+
this.request = request;
|
|
124
|
+
this.response = response;
|
|
125
|
+
this.metadata = metadata;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Usage
|
|
130
|
+
const meta = new MethodMeta(
|
|
131
|
+
'POST',
|
|
132
|
+
'/api/save',
|
|
133
|
+
'save',
|
|
134
|
+
[requestBody],
|
|
135
|
+
requestData,
|
|
136
|
+
undefined,
|
|
137
|
+
new Map()
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Pattern 4: Extending Data Classes
|
|
142
|
+
|
|
143
|
+
When one data class extends another:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
export class RegisteredRoute<TResult = unknown> extends RouteDefinition<TResult> {
|
|
147
|
+
routeMetadata?: RouteMetadata;
|
|
148
|
+
controllerClass?: any;
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
method: string,
|
|
152
|
+
path: string,
|
|
153
|
+
handler: RouteHandler<TResult>,
|
|
154
|
+
controllerFilepath?: string,
|
|
155
|
+
routeMetadata?: RouteMetadata,
|
|
156
|
+
controllerClass?: any
|
|
157
|
+
) {
|
|
158
|
+
super(method, path, handler, controllerFilepath);
|
|
159
|
+
this.routeMetadata = routeMetadata;
|
|
160
|
+
this.controllerClass = controllerClass;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Filter Chain Patterns
|
|
168
|
+
|
|
169
|
+
### Pattern 1: Global Filter
|
|
170
|
+
|
|
171
|
+
Applies to all routes:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
export class FilterRoutes implements Routes {
|
|
175
|
+
configure(routeBuilder: RouteBuilder): void {
|
|
176
|
+
routeBuilder.addFilter(
|
|
177
|
+
new FilterDefinition(140, ContextFilter, '*')
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Pattern 2: Path-Scoped Filter
|
|
184
|
+
|
|
185
|
+
Applies to controllers matching a pattern:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// All admin controllers
|
|
189
|
+
routeBuilder.addFilter(
|
|
190
|
+
new FilterDefinition(
|
|
191
|
+
100,
|
|
192
|
+
AdminAuthFilter,
|
|
193
|
+
'src/controllers/admin/**/*.ts'
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Specific controller
|
|
198
|
+
routeBuilder.addFilter(
|
|
199
|
+
new FilterDefinition(
|
|
200
|
+
80,
|
|
201
|
+
SpecialFilter,
|
|
202
|
+
'**/SaveController.ts'
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Any controller in 'admin' directory
|
|
207
|
+
routeBuilder.addFilter(
|
|
208
|
+
new FilterDefinition(
|
|
209
|
+
90,
|
|
210
|
+
AdminFilter,
|
|
211
|
+
'**/admin/**'
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Pattern 3: Filter Implementation
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { injectable } from 'inversify';
|
|
220
|
+
import { Filter, MethodMeta, Action, NextFilter } from '@webpieces/http-filters';
|
|
221
|
+
|
|
222
|
+
@injectable()
|
|
223
|
+
export class MyFilter implements Filter {
|
|
224
|
+
priority = 100; // Higher = executes earlier
|
|
225
|
+
|
|
226
|
+
async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
|
|
227
|
+
// 1. Pre-processing
|
|
228
|
+
console.log(`Before: ${meta.httpMethod} ${meta.path}`);
|
|
229
|
+
|
|
230
|
+
// 2. Call next filter/controller
|
|
231
|
+
const action = await next.execute();
|
|
232
|
+
|
|
233
|
+
// 3. Post-processing
|
|
234
|
+
console.log(`After: ${action.statusCode}`);
|
|
235
|
+
|
|
236
|
+
return action;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Pattern 4: Filter Priority Convention
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
140 - Context setup (ContextFilter)
|
|
245
|
+
120 - Request attributes
|
|
246
|
+
100 - Authorization/Authentication
|
|
247
|
+
90 - Metrics
|
|
248
|
+
80 - Logging
|
|
249
|
+
60 - JSON serialization (JsonFilter)
|
|
250
|
+
40 - Transactions
|
|
251
|
+
20 - Caching
|
|
252
|
+
0 - Controller execution
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Dependency Injection Patterns
|
|
258
|
+
|
|
259
|
+
### Pattern 1: Controller with Dependencies
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { injectable, inject } from 'inversify';
|
|
263
|
+
import { provideSingleton, Controller } from '@webpieces/http-routing';
|
|
264
|
+
|
|
265
|
+
@provideSingleton()
|
|
266
|
+
@Controller()
|
|
267
|
+
export class SaveController extends SaveApiPrototype implements SaveApi {
|
|
268
|
+
private readonly __validator!: ValidateImplementation<SaveController, SaveApi>;
|
|
269
|
+
|
|
270
|
+
constructor(
|
|
271
|
+
@inject(TYPES.Counter) private counter: Counter,
|
|
272
|
+
@inject(TYPES.RemoteApi) private remoteService: RemoteApi
|
|
273
|
+
) {
|
|
274
|
+
super();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
override async save(request: SaveRequest): Promise<SaveResponse> {
|
|
278
|
+
// Implementation
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Pattern 2: Filter with Unmanaged Config
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
@injectable()
|
|
287
|
+
export class JsonFilter implements Filter {
|
|
288
|
+
constructor(
|
|
289
|
+
@unmanaged() private config: JsonFilterConfig = new JsonFilterConfig()
|
|
290
|
+
) {
|
|
291
|
+
// config is not injected from DI container
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Pattern 3: DI Module Registration
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { ContainerModule } from 'inversify';
|
|
300
|
+
import { buildProviderModule } from '@inversifyjs/binding-decorators';
|
|
301
|
+
|
|
302
|
+
export class MyModule {
|
|
303
|
+
getModule(): ContainerModule {
|
|
304
|
+
return new ContainerModule((bind) => {
|
|
305
|
+
// Manual bindings
|
|
306
|
+
bind<Counter>(TYPES.Counter).to(SimpleCounter).inSingletonScope();
|
|
307
|
+
|
|
308
|
+
// Auto-scan for @provideSingleton decorators
|
|
309
|
+
// (handled by buildProviderModule)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Decorator Patterns
|
|
318
|
+
|
|
319
|
+
### Pattern 1: API Interface Declaration
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { ApiInterface, Post, Path } from '@webpieces/http-api';
|
|
323
|
+
|
|
324
|
+
@ApiInterface()
|
|
325
|
+
export abstract class SaveApiPrototype {
|
|
326
|
+
@Post()
|
|
327
|
+
@Path('/search/item')
|
|
328
|
+
abstract save(request: SaveRequest): Promise<SaveResponse>;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface SaveApi extends SaveApiPrototype {}
|
|
332
|
+
export const SaveApiPrototype = SaveApiPrototype;
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Pattern 2: Controller Implementation
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { Controller, provideSingleton, SourceFile } from '@webpieces/http-routing';
|
|
339
|
+
|
|
340
|
+
@SourceFile('src/controllers/SaveController.ts') // Optional: explicit filepath
|
|
341
|
+
@provideSingleton()
|
|
342
|
+
@Controller()
|
|
343
|
+
export class SaveController extends SaveApiPrototype implements SaveApi {
|
|
344
|
+
override async save(request: SaveRequest): Promise<SaveResponse> {
|
|
345
|
+
// Implementation
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Pattern 3: Validation Helper
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { ValidateImplementation } from '@webpieces/http-api';
|
|
354
|
+
|
|
355
|
+
export class SaveController extends SaveApiPrototype implements SaveApi {
|
|
356
|
+
// Compile-time validator: Ensures all SaveApi methods are implemented
|
|
357
|
+
private readonly __validator!: ValidateImplementation<SaveController, SaveApi>;
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Testing Patterns
|
|
364
|
+
|
|
365
|
+
### Pattern 1: Unit Test for Filter Matching
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { FilterMatcher } from './FilterMatcher';
|
|
369
|
+
import { FilterDefinition } from '@webpieces/core-meta';
|
|
370
|
+
|
|
371
|
+
describe('FilterMatcher', () => {
|
|
372
|
+
it('should match admin controllers', () => {
|
|
373
|
+
const adminFilter = new MockFilter(100);
|
|
374
|
+
|
|
375
|
+
const registry = [
|
|
376
|
+
{
|
|
377
|
+
filter: adminFilter,
|
|
378
|
+
definition: new FilterDefinition(
|
|
379
|
+
100,
|
|
380
|
+
MockFilter,
|
|
381
|
+
'src/controllers/admin/**/*.ts'
|
|
382
|
+
),
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
const result = FilterMatcher.findMatchingFilters(
|
|
387
|
+
'src/controllers/admin/UserController.ts',
|
|
388
|
+
registry
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
expect(result).toEqual([adminFilter]);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Pattern 2: Integration Test without HTTP
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
import { WebpiecesServer } from '@webpieces/http-server';
|
|
400
|
+
import { ProdServerMeta } from '../ProdServerMeta';
|
|
401
|
+
import { SaveApi, SaveApiPrototype } from '../api/SaveApi';
|
|
402
|
+
|
|
403
|
+
describe('SaveApi Integration', () => {
|
|
404
|
+
let server: WebpiecesServer;
|
|
405
|
+
let saveApi: SaveApi;
|
|
406
|
+
|
|
407
|
+
beforeEach(() => {
|
|
408
|
+
server = new WebpiecesServer(new ProdServerMeta());
|
|
409
|
+
server.initialize();
|
|
410
|
+
saveApi = server.createApiClient<SaveApi>(SaveApiPrototype);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should process request through filter chain', async () => {
|
|
414
|
+
const request = new SaveRequest('test query');
|
|
415
|
+
const response = await saveApi.save(request);
|
|
416
|
+
|
|
417
|
+
expect(response.success).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Pattern 3: HTTP Client Test with Mock
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { createClient, ClientConfig } from '@webpieces/http-client';
|
|
426
|
+
|
|
427
|
+
describe('SaveApi Client', () => {
|
|
428
|
+
let mockFetch: jest.Mock;
|
|
429
|
+
let originalFetch: typeof fetch;
|
|
430
|
+
|
|
431
|
+
beforeEach(() => {
|
|
432
|
+
originalFetch = global.fetch;
|
|
433
|
+
mockFetch = jest.fn();
|
|
434
|
+
global.fetch = mockFetch as any;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
afterEach(() => {
|
|
438
|
+
global.fetch = originalFetch;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should make HTTP request', async () => {
|
|
442
|
+
mockFetch.mockResolvedValue({
|
|
443
|
+
ok: true,
|
|
444
|
+
json: async () => ({ success: true }),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const config = new ClientConfig('http://localhost:3000');
|
|
448
|
+
const client = createClient(SaveApiPrototype, config);
|
|
449
|
+
|
|
450
|
+
const response = await client.save(new SaveRequest('test'));
|
|
451
|
+
|
|
452
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
453
|
+
'http://localhost:3000/search/item',
|
|
454
|
+
expect.objectContaining({
|
|
455
|
+
method: 'POST',
|
|
456
|
+
body: JSON.stringify({ query: 'test' }),
|
|
457
|
+
})
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## File Organization Patterns
|
|
466
|
+
|
|
467
|
+
### Package Structure
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
packages/
|
|
471
|
+
core/
|
|
472
|
+
core-meta/ - Core type definitions (RouteDefinition, FilterDefinition, etc.)
|
|
473
|
+
core-context/ - AsyncLocalStorage context management
|
|
474
|
+
http/
|
|
475
|
+
http-api/ - API decorators (shared by client & server)
|
|
476
|
+
http-routing/ - Server-side routing (RESTApiRoutes)
|
|
477
|
+
http-client/ - Client-side HTTP client generation
|
|
478
|
+
http-filters/ - Filter implementations
|
|
479
|
+
http-server/ - WebpiecesServer, FilterMatcher
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Export Pattern
|
|
483
|
+
|
|
484
|
+
Always export from `index.ts`:
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
// packages/http/http-client/src/index.ts
|
|
488
|
+
export { createClient, ClientConfig } from './ClientFactory';
|
|
489
|
+
|
|
490
|
+
// Re-export API decorators for convenience
|
|
491
|
+
export {
|
|
492
|
+
ApiInterface,
|
|
493
|
+
Post,
|
|
494
|
+
Get,
|
|
495
|
+
Path,
|
|
496
|
+
} from '@webpieces/http-api';
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Migration from Java Patterns
|
|
502
|
+
|
|
503
|
+
When porting features from Java webpieces:
|
|
504
|
+
|
|
505
|
+
### Java → TypeScript Equivalents
|
|
506
|
+
|
|
507
|
+
| Java | TypeScript |
|
|
508
|
+
|------|------------|
|
|
509
|
+
| `interface` (data) | `class` |
|
|
510
|
+
| `interface` (with methods) | `interface` |
|
|
511
|
+
| Guice | Inversify |
|
|
512
|
+
| `@Inject` | `@inject(TYPES.Something)` |
|
|
513
|
+
| `@Singleton` | `@provideSingleton()` |
|
|
514
|
+
| Package regex | Filepath glob pattern |
|
|
515
|
+
| `Pattern.compile("...")` | `minimatch(path, pattern)` |
|
|
516
|
+
| JAX-RS annotations | Decorators (`@Post`, `@Path`) |
|
|
517
|
+
|
|
518
|
+
### Common Conversions
|
|
519
|
+
|
|
520
|
+
**Java Filter:**
|
|
521
|
+
```java
|
|
522
|
+
public class MyFilter implements RouteFilter {
|
|
523
|
+
@Inject
|
|
524
|
+
public MyFilter(SomeService service) {
|
|
525
|
+
this.service = service;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
@Override
|
|
529
|
+
public CompletableFuture<Action> filter(MethodMeta meta, Service<MethodMeta, Action> next) {
|
|
530
|
+
// Logic
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**TypeScript Filter:**
|
|
536
|
+
```typescript
|
|
537
|
+
@injectable()
|
|
538
|
+
export class MyFilter implements Filter {
|
|
539
|
+
constructor(
|
|
540
|
+
@inject(TYPES.SomeService) private service: SomeService
|
|
541
|
+
) {}
|
|
542
|
+
|
|
543
|
+
async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
|
|
544
|
+
// Logic
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Anti-Patterns to Avoid
|
|
552
|
+
|
|
553
|
+
### ❌ Anonymous Object Literals for Data
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// BAD
|
|
557
|
+
routeBuilder.addRoute({
|
|
558
|
+
method: 'POST',
|
|
559
|
+
path: '/api/save',
|
|
560
|
+
handler: myHandler,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// GOOD
|
|
564
|
+
routeBuilder.addRoute(
|
|
565
|
+
new RouteDefinition('POST', '/api/save', myHandler)
|
|
566
|
+
);
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### ❌ Interface for Data-Only Structures
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
// BAD
|
|
573
|
+
export interface UserConfig {
|
|
574
|
+
name: string;
|
|
575
|
+
age: number;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const config = { name: 'John', age: 30 }; // Anonymous
|
|
579
|
+
|
|
580
|
+
// GOOD
|
|
581
|
+
export class UserConfig {
|
|
582
|
+
name: string;
|
|
583
|
+
age: number;
|
|
584
|
+
|
|
585
|
+
constructor(name: string, age: number) {
|
|
586
|
+
this.name = name;
|
|
587
|
+
this.age = age;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const config = new UserConfig('John', 30); // Explicit
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### ❌ Using 'any' Instead of 'unknown'
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
// BAD
|
|
598
|
+
export class RouteHandler<TResult = any> {
|
|
599
|
+
abstract execute(context: RouteContext): Promise<TResult>;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// GOOD
|
|
603
|
+
export class RouteHandler<TResult = unknown> {
|
|
604
|
+
abstract execute(context: RouteContext): Promise<TResult>;
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### ❌ Not Exporting Helper Functions
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
// BAD - Helper function not exported
|
|
612
|
+
function jsonAction(data: any): Action {
|
|
613
|
+
return new Action('json', data);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// GOOD - Helper function exported for reuse
|
|
617
|
+
export function jsonAction(data: any, statusCode: number = 200): Action {
|
|
618
|
+
return new Action('json', data, statusCode);
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Advanced Patterns
|
|
625
|
+
|
|
626
|
+
### Pattern 1: Type-Safe API Client
|
|
627
|
+
|
|
628
|
+
The client generator creates type-safe proxies:
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// Define API interface
|
|
632
|
+
@ApiInterface()
|
|
633
|
+
export abstract class SaveApiPrototype {
|
|
634
|
+
@Post()
|
|
635
|
+
@Path('/search/item')
|
|
636
|
+
abstract save(request: SaveRequest): Promise<SaveResponse>;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Create client
|
|
640
|
+
const config = new ClientConfig('http://localhost:3000');
|
|
641
|
+
const client = createClient(SaveApiPrototype, config);
|
|
642
|
+
|
|
643
|
+
// Type-safe method call
|
|
644
|
+
const response: SaveResponse = await client.save(request); // ✓ Type checked
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Pattern 2: Filter Chain Execution
|
|
648
|
+
|
|
649
|
+
```
|
|
650
|
+
Request → Filter 1 (priority 140) → Filter 2 (priority 60) → Controller
|
|
651
|
+
↓ ↓ ↓
|
|
652
|
+
wraps next wraps next returns result
|
|
653
|
+
↓ ↓ ↓
|
|
654
|
+
Response ← modifies response ← modifies response ← original result
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Implementation:
|
|
658
|
+
```typescript
|
|
659
|
+
@injectable()
|
|
660
|
+
export class LoggingFilter implements Filter {
|
|
661
|
+
priority = 80;
|
|
662
|
+
|
|
663
|
+
async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
|
|
664
|
+
const start = Date.now();
|
|
665
|
+
|
|
666
|
+
// Execute next in chain
|
|
667
|
+
const action = await next.execute();
|
|
668
|
+
|
|
669
|
+
const duration = Date.now() - start;
|
|
670
|
+
console.log(`${meta.httpMethod} ${meta.path} - ${duration}ms`);
|
|
671
|
+
|
|
672
|
+
return action;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Pattern 3: Context Management with AsyncLocalStorage
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
import { Context } from '@webpieces/core-context';
|
|
681
|
+
|
|
682
|
+
// In ContextFilter (priority 140 - executes first)
|
|
683
|
+
@injectable()
|
|
684
|
+
export class ContextFilter implements Filter {
|
|
685
|
+
priority = 140;
|
|
686
|
+
|
|
687
|
+
async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
|
|
688
|
+
return Context.run(() => {
|
|
689
|
+
Context.set('REQUEST_PATH', meta.path);
|
|
690
|
+
Context.set('START_TIME', Date.now());
|
|
691
|
+
return next.execute();
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// In any controller or filter
|
|
697
|
+
const path = Context.get('REQUEST_PATH');
|
|
698
|
+
const startTime = Context.get('START_TIME');
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Pattern 4: Two-Container DI Pattern
|
|
702
|
+
|
|
703
|
+
Similar to Java WebPieces:
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
export class WebpiecesServer {
|
|
707
|
+
// Framework-level bindings
|
|
708
|
+
private webpiecesContainer: Container;
|
|
709
|
+
|
|
710
|
+
// Application bindings (child of webpiecesContainer)
|
|
711
|
+
private appContainer: Container;
|
|
712
|
+
|
|
713
|
+
constructor(meta: WebAppMeta) {
|
|
714
|
+
this.webpiecesContainer = new Container();
|
|
715
|
+
this.appContainer = new Container({ parent: this.webpiecesContainer });
|
|
716
|
+
|
|
717
|
+
// Load user modules into app container
|
|
718
|
+
const modules = meta.getDIModules();
|
|
719
|
+
for (const module of modules) {
|
|
720
|
+
this.appContainer.load(module);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## Filepath-Based Filter Matching
|
|
729
|
+
|
|
730
|
+
### How It Works
|
|
731
|
+
|
|
732
|
+
Similar to Java's `SharedMatchUtil.findMatchingFilters()`:
|
|
733
|
+
|
|
734
|
+
1. **Route Registration**: Controller filepath is captured during route registration
|
|
735
|
+
- Uses `@SourceFile()` decorator if present
|
|
736
|
+
- Falls back to class name pattern: `**/SaveController.ts`
|
|
737
|
+
|
|
738
|
+
2. **Filter Matching**: At startup, `FilterMatcher` matches filters to routes
|
|
739
|
+
- Pattern `'*'` matches all controllers (global)
|
|
740
|
+
- Pattern `'src/controllers/admin/**/*.ts'` matches admin controllers
|
|
741
|
+
- Uses `minimatch` library for glob pattern matching
|
|
742
|
+
|
|
743
|
+
3. **Filter Chain Creation**: Matched filters are sorted by priority and cached
|
|
744
|
+
- No runtime overhead - matching happens once at startup
|
|
745
|
+
- Each route gets its own filter chain
|
|
746
|
+
|
|
747
|
+
### Controller Filepath Extraction
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
private getControllerFilepath(): string | undefined {
|
|
751
|
+
// 1. Check for explicit @SourceFile decorator
|
|
752
|
+
const filepath = Reflect.getMetadata(
|
|
753
|
+
ROUTING_METADATA_KEYS.SOURCE_FILEPATH,
|
|
754
|
+
this.controllerClass
|
|
755
|
+
);
|
|
756
|
+
if (filepath) {
|
|
757
|
+
return filepath;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// 2. Fallback to class name pattern
|
|
761
|
+
const className = (this.controllerClass as any).name;
|
|
762
|
+
return className ? `**/${className}.ts` : undefined;
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### Glob Pattern Examples
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
'*' // All controllers (global)
|
|
770
|
+
'**/*' // All controllers (alternative)
|
|
771
|
+
'src/controllers/**/*.ts' // All controllers in src/controllers
|
|
772
|
+
'src/controllers/admin/**/*.ts' // All admin controllers
|
|
773
|
+
'**/admin/**' // Any file in admin directory
|
|
774
|
+
'**/SaveController.ts' // Specific controller file
|
|
775
|
+
'apps/example-app/src/**/*.ts' // All controllers in example-app
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
## Summary Checklist
|
|
781
|
+
|
|
782
|
+
When adding new code to webpieces-ts:
|
|
783
|
+
|
|
784
|
+
- [ ] Is it data-only? → Use `class`, not `interface`
|
|
785
|
+
- [ ] Does it have business logic methods? → Use `interface`
|
|
786
|
+
- [ ] Are you creating config/metadata? → Use `class` with constructor defaults
|
|
787
|
+
- [ ] Adding a new filter? → Implement `Filter` interface, use `@injectable()`
|
|
788
|
+
- [ ] Adding a new controller? → Extend API prototype, use `@Controller()` and `@provideSingleton()`
|
|
789
|
+
- [ ] Need to scope a filter? → Use `filepathPattern` in `FilterDefinition`
|
|
790
|
+
- [ ] Writing tests? → Unit tests for logic, integration tests for behavior
|
|
791
|
+
- [ ] Updated exports? → Add to package's `index.ts`
|
|
792
|
+
- [ ] Documented patterns? → Update this file if introducing new patterns
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## Questions?
|
|
797
|
+
|
|
798
|
+
See `CLAUDE.md` for higher-level guidelines and architecture overview.
|