sandly 2.0.0 → 2.1.0
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 +240 -133
- package/dist/index.d.ts +57 -5
- package/dist/index.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sandly
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sandly ("Services And Layers") is a type-safe dependency injection library for TypeScript. No decorators, no runtime reflection, just compile-time safety that catches errors before your code runs.
|
|
4
4
|
|
|
5
5
|
## Why Sandly?
|
|
6
6
|
|
|
@@ -10,12 +10,16 @@ Most TypeScript DI libraries rely on experimental decorators and runtime reflect
|
|
|
10
10
|
import { Container, Layer } from 'sandly';
|
|
11
11
|
|
|
12
12
|
class Database {
|
|
13
|
-
|
|
13
|
+
query(sql: string) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
class UserService {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
constructor(private db: Database) {}
|
|
20
|
+
getUsers() {
|
|
21
|
+
return this.db.query('SELECT * FROM users');
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
// Define layers
|
|
@@ -60,24 +64,24 @@ import { Container, Layer, Tag } from 'sandly';
|
|
|
60
64
|
|
|
61
65
|
// Any class can be a dependency - no special base class needed
|
|
62
66
|
class Database {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
async query(sql: string) {
|
|
68
|
+
return [{ id: 1, name: 'Alice' }];
|
|
69
|
+
}
|
|
70
|
+
async close() {
|
|
71
|
+
console.log('Database closed');
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
class UserRepository {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
constructor(private db: Database) {}
|
|
77
|
+
findAll() {
|
|
78
|
+
return this.db.query('SELECT * FROM users');
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
// Create layers
|
|
79
83
|
const dbLayer = Layer.service(Database, [], {
|
|
80
|
-
|
|
84
|
+
cleanup: (db) => db.close(),
|
|
81
85
|
});
|
|
82
86
|
|
|
83
87
|
const userRepoLayer = Layer.service(UserRepository, [Database]);
|
|
@@ -104,7 +108,9 @@ Tags identify dependencies. There are two types:
|
|
|
104
108
|
|
|
105
109
|
```typescript
|
|
106
110
|
class UserService {
|
|
107
|
-
|
|
111
|
+
getUsers() {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
// UserService is both the class and its tag
|
|
@@ -118,7 +124,9 @@ const PortTag = Tag.of('Port')<number>();
|
|
|
118
124
|
const ConfigTag = Tag.of('Config')<{ apiUrl: string }>();
|
|
119
125
|
|
|
120
126
|
const portLayer = Layer.value(PortTag, 3000);
|
|
121
|
-
const configLayer = Layer.value(ConfigTag, {
|
|
127
|
+
const configLayer = Layer.value(ConfigTag, {
|
|
128
|
+
apiUrl: 'https://api.example.com',
|
|
129
|
+
});
|
|
122
130
|
```
|
|
123
131
|
|
|
124
132
|
### Container
|
|
@@ -131,19 +139,20 @@ const container = Container.from(appLayer);
|
|
|
131
139
|
|
|
132
140
|
// Or build manually
|
|
133
141
|
const container = Container.builder()
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
.add(Database, () => new Database())
|
|
143
|
+
.add(
|
|
144
|
+
UserService,
|
|
145
|
+
async (ctx) => new UserService(await ctx.resolve(Database))
|
|
146
|
+
)
|
|
147
|
+
.build();
|
|
139
148
|
|
|
140
149
|
// Resolve dependencies
|
|
141
150
|
const db = await container.resolve(Database);
|
|
142
151
|
const [db, users] = await container.resolveAll(Database, UserService);
|
|
143
152
|
|
|
144
153
|
// Use and discard pattern - resolves, runs callback, then destroys
|
|
145
|
-
const result = await container.use(UserService, (service) =>
|
|
146
|
-
|
|
154
|
+
const result = await container.use(UserService, (service) =>
|
|
155
|
+
service.getUsers()
|
|
147
156
|
);
|
|
148
157
|
|
|
149
158
|
// Manual clean up
|
|
@@ -165,11 +174,12 @@ const configLayer = Layer.value(ConfigTag, { port: 3000 });
|
|
|
165
174
|
|
|
166
175
|
// Layer.create for custom factory logic
|
|
167
176
|
const cacheLayer = Layer.create({
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
requires: [ConfigTag],
|
|
178
|
+
apply: (builder) =>
|
|
179
|
+
builder.add(Cache, async (ctx) => {
|
|
180
|
+
const config = await ctx.resolve(ConfigTag);
|
|
181
|
+
return new Cache({ ttl: config.cacheTtl });
|
|
182
|
+
}),
|
|
173
183
|
});
|
|
174
184
|
```
|
|
175
185
|
|
|
@@ -195,16 +205,17 @@ Scoped containers enable hierarchical dependency management:
|
|
|
195
205
|
```typescript
|
|
196
206
|
// Application scope - use builder to add dependencies
|
|
197
207
|
const appContainer = ScopedContainer.builder('app')
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
.add(Database, () => new Database())
|
|
209
|
+
.build();
|
|
200
210
|
|
|
201
211
|
// Request scope - use child() to create a child builder
|
|
202
|
-
const requestContainer = appContainer
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
const requestContainer = appContainer
|
|
213
|
+
.child('request')
|
|
214
|
+
.add(RequestContext, () => new RequestContext())
|
|
215
|
+
.build();
|
|
205
216
|
|
|
206
217
|
// Child can resolve both its own and parent dependencies
|
|
207
|
-
const db = await requestContainer.resolve(Database);
|
|
218
|
+
const db = await requestContainer.resolve(Database); // From parent
|
|
208
219
|
const ctx = await requestContainer.resolve(RequestContext); // From child
|
|
209
220
|
|
|
210
221
|
// Destroy child without affecting parent
|
|
@@ -215,8 +226,9 @@ Or use layers with `childFrom`:
|
|
|
215
226
|
|
|
216
227
|
```typescript
|
|
217
228
|
const appContainer = ScopedContainer.from('app', dbLayer);
|
|
218
|
-
const requestContainer = appContainer.childFrom(
|
|
219
|
-
|
|
229
|
+
const requestContainer = appContainer.childFrom(
|
|
230
|
+
'request',
|
|
231
|
+
Layer.value(RequestContext, new RequestContext())
|
|
220
232
|
);
|
|
221
233
|
```
|
|
222
234
|
|
|
@@ -227,8 +239,8 @@ The `use()` method resolves a service, runs a callback, and automatically destro
|
|
|
227
239
|
```typescript
|
|
228
240
|
// Perfect for short-lived operations like Lambda handlers or worker jobs
|
|
229
241
|
const result = await appContainer
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
.childFrom('request', requestLayer)
|
|
243
|
+
.use(UserService, (service) => service.processEvent(event));
|
|
232
244
|
// Container is automatically destroyed after callback completes
|
|
233
245
|
```
|
|
234
246
|
|
|
@@ -242,7 +254,10 @@ This is especially useful for serverless functions or message handlers where the
|
|
|
242
254
|
|
|
243
255
|
```typescript
|
|
244
256
|
class ApiClient {
|
|
245
|
-
|
|
257
|
+
constructor(
|
|
258
|
+
private config: Config,
|
|
259
|
+
private logger: Logger
|
|
260
|
+
) {}
|
|
246
261
|
}
|
|
247
262
|
|
|
248
263
|
// Dependencies must match constructor parameters in order
|
|
@@ -250,28 +265,60 @@ const apiLayer = Layer.service(ApiClient, [Config, Logger]);
|
|
|
250
265
|
|
|
251
266
|
// With cleanup function
|
|
252
267
|
const dbLayer = Layer.service(Database, [], {
|
|
253
|
-
|
|
268
|
+
cleanup: (db) => db.close(),
|
|
254
269
|
});
|
|
255
270
|
```
|
|
256
271
|
|
|
257
|
-
**Layer.value**: Constant values
|
|
272
|
+
**Layer.value**: Constant values or pre-instantiated instances
|
|
258
273
|
|
|
259
274
|
```typescript
|
|
275
|
+
// ValueTag (constants)
|
|
260
276
|
const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
261
277
|
const configLayer = Layer.value(ApiKeyTag, process.env.API_KEY!);
|
|
278
|
+
|
|
279
|
+
// ServiceTag (pre-instantiated instances)
|
|
280
|
+
class UserService {
|
|
281
|
+
getUsers() {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const userService = new UserService();
|
|
286
|
+
const testLayer = Layer.value(UserService, userService);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Layer.mock**: Partial mocks for testing (ServiceTags only)
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
class UserService {
|
|
293
|
+
constructor(private db: Database) {}
|
|
294
|
+
getUsers() {
|
|
295
|
+
return this.db.query('SELECT * FROM users');
|
|
296
|
+
}
|
|
297
|
+
getUserById(id: number) {
|
|
298
|
+
return this.db.query(`...`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Mock only the methods you need - no constructor dependencies required
|
|
303
|
+
const testLayer = Layer.mock(UserService, {
|
|
304
|
+
getUsers: () => Promise.resolve([{ id: 1, name: 'Alice' }]),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// TypeScript still validates the mock's method signatures
|
|
262
308
|
```
|
|
263
309
|
|
|
264
310
|
**Layer.create**: Custom factory logic
|
|
265
311
|
|
|
266
312
|
```typescript
|
|
267
313
|
const dbLayer = Layer.create({
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
314
|
+
requires: [ConfigTag],
|
|
315
|
+
apply: (builder) =>
|
|
316
|
+
builder.add(Database, async (ctx) => {
|
|
317
|
+
const config = await ctx.resolve(ConfigTag);
|
|
318
|
+
const db = new Database(config.dbUrl);
|
|
319
|
+
await db.connect();
|
|
320
|
+
return db;
|
|
321
|
+
}),
|
|
275
322
|
});
|
|
276
323
|
```
|
|
277
324
|
|
|
@@ -286,10 +333,10 @@ const serviceLayer = Layer.service(UserService, [UserRepository, Logger]);
|
|
|
286
333
|
|
|
287
334
|
// Compose into complete application
|
|
288
335
|
const appLayer = serviceLayer
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
336
|
+
.provide(repoLayer)
|
|
337
|
+
.provide(dbLayer)
|
|
338
|
+
.provide(configLayer)
|
|
339
|
+
.provide(Layer.service(Logger, []));
|
|
293
340
|
|
|
294
341
|
// Create container - all dependencies satisfied
|
|
295
342
|
const container = Container.from(appLayer);
|
|
@@ -324,48 +371,55 @@ const container = Container.from(incomplete); // Type error!
|
|
|
324
371
|
import { ScopedContainer, Layer } from 'sandly';
|
|
325
372
|
|
|
326
373
|
// App-level dependencies (shared across requests)
|
|
327
|
-
const appContainer = ScopedContainer.from(
|
|
328
|
-
|
|
374
|
+
const appContainer = ScopedContainer.from(
|
|
375
|
+
'app',
|
|
376
|
+
Layer.mergeAll(dbLayer, loggerLayer)
|
|
329
377
|
);
|
|
330
378
|
|
|
331
379
|
// Express middleware
|
|
332
380
|
app.use(async (req, res, next) => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
381
|
+
// Create request scope with request-specific dependencies
|
|
382
|
+
const requestScope = appContainer.childFrom(
|
|
383
|
+
'request',
|
|
384
|
+
Layer.value(RequestContext, {
|
|
385
|
+
requestId: crypto.randomUUID(),
|
|
386
|
+
userId: req.user?.id,
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
res.locals.container = requestScope;
|
|
391
|
+
|
|
392
|
+
res.on('finish', () => requestScope.destroy());
|
|
393
|
+
next();
|
|
345
394
|
});
|
|
346
395
|
|
|
347
396
|
// Route handler
|
|
348
397
|
app.get('/users', async (req, res) => {
|
|
349
|
-
|
|
350
|
-
|
|
398
|
+
const userService = await res.locals.container.resolve(UserService);
|
|
399
|
+
res.json(await userService.getUsers());
|
|
351
400
|
});
|
|
352
401
|
```
|
|
353
402
|
|
|
354
403
|
### Destruction Order
|
|
355
404
|
|
|
356
405
|
When destroying a scoped container:
|
|
406
|
+
|
|
357
407
|
1. Child scopes are destroyed first
|
|
358
408
|
2. Then the current scope's finalizers run
|
|
359
409
|
3. Parent scope is unaffected
|
|
360
410
|
|
|
361
411
|
```typescript
|
|
362
412
|
const parent = ScopedContainer.builder('parent')
|
|
363
|
-
|
|
364
|
-
|
|
413
|
+
.add(Database, {
|
|
414
|
+
create: () => new Database(),
|
|
415
|
+
cleanup: (db) => db.close(),
|
|
416
|
+
})
|
|
417
|
+
.build();
|
|
365
418
|
|
|
366
|
-
const child = parent
|
|
367
|
-
|
|
368
|
-
|
|
419
|
+
const child = parent
|
|
420
|
+
.child('child')
|
|
421
|
+
.add(Cache, { create: () => new Cache(), cleanup: (c) => c.clear() })
|
|
422
|
+
.build();
|
|
369
423
|
|
|
370
424
|
await parent.destroy(); // Destroys child first (Cache.clear), then parent (Database.close)
|
|
371
425
|
```
|
|
@@ -376,25 +430,25 @@ Sandly provides specific error types for common issues:
|
|
|
376
430
|
|
|
377
431
|
```typescript
|
|
378
432
|
import {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
433
|
+
UnknownDependencyError,
|
|
434
|
+
CircularDependencyError,
|
|
435
|
+
DependencyCreationError,
|
|
436
|
+
DependencyFinalizationError,
|
|
383
437
|
} from 'sandly';
|
|
384
438
|
|
|
385
439
|
try {
|
|
386
|
-
|
|
440
|
+
const service = await container.resolve(UserService);
|
|
387
441
|
} catch (error) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
442
|
+
if (error instanceof CircularDependencyError) {
|
|
443
|
+
console.log(error.message);
|
|
444
|
+
// "Circular dependency detected for UserService: UserService -> Database -> UserService"
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (error instanceof DependencyCreationError) {
|
|
448
|
+
// Get the original error that caused the failure
|
|
449
|
+
const rootCause = error.getRootCause();
|
|
450
|
+
console.log(rootCause.message);
|
|
451
|
+
}
|
|
398
452
|
}
|
|
399
453
|
```
|
|
400
454
|
|
|
@@ -402,68 +456,121 @@ try {
|
|
|
402
456
|
|
|
403
457
|
### Container
|
|
404
458
|
|
|
405
|
-
| Method
|
|
406
|
-
|
|
407
|
-
| `Container.from(layer)`
|
|
408
|
-
| `Container.builder()`
|
|
409
|
-
| `Container.empty()`
|
|
410
|
-
| `Container.scoped(scope)`
|
|
411
|
-
| `container.resolve(tag)`
|
|
412
|
-
| `container.resolveAll(...tags)` | Get multiple dependencies
|
|
413
|
-
| `container.use(tag, fn)`
|
|
414
|
-
| `container.destroy()`
|
|
459
|
+
| Method | Description |
|
|
460
|
+
| ------------------------------- | --------------------------------------------- |
|
|
461
|
+
| `Container.from(layer)` | Create container from a fully resolved layer |
|
|
462
|
+
| `Container.builder()` | Create a container builder |
|
|
463
|
+
| `Container.empty()` | Create an empty container |
|
|
464
|
+
| `Container.scoped(scope)` | Create an empty scoped container |
|
|
465
|
+
| `container.resolve(tag)` | Get a dependency instance |
|
|
466
|
+
| `container.resolveAll(...tags)` | Get multiple dependencies |
|
|
467
|
+
| `container.use(tag, fn)` | Resolve, run callback, then destroy container |
|
|
468
|
+
| `container.destroy()` | Run finalizers and clean up |
|
|
415
469
|
|
|
416
470
|
### ContainerBuilder
|
|
417
471
|
|
|
418
|
-
| Method
|
|
419
|
-
|
|
472
|
+
| Method | Description |
|
|
473
|
+
| ------------------------ | --------------------- |
|
|
420
474
|
| `builder.add(tag, spec)` | Register a dependency |
|
|
421
|
-
| `builder.build()`
|
|
475
|
+
| `builder.build()` | Create the container |
|
|
422
476
|
|
|
423
477
|
### Layer
|
|
424
478
|
|
|
425
|
-
| Method
|
|
426
|
-
|
|
427
|
-
| `Layer.service(class, deps, options?)` | Create layer for a class
|
|
428
|
-
| `Layer.value(tag, value)`
|
|
429
|
-
| `Layer.
|
|
430
|
-
| `Layer.
|
|
431
|
-
| `Layer.
|
|
432
|
-
| `Layer.
|
|
433
|
-
| `
|
|
434
|
-
| `layer.
|
|
435
|
-
| `layer.
|
|
479
|
+
| Method | Description |
|
|
480
|
+
| -------------------------------------- | ----------------------------------------------- |
|
|
481
|
+
| `Layer.service(class, deps, options?)` | Create layer for a class |
|
|
482
|
+
| `Layer.value(tag, value)` | Create layer for a constant value |
|
|
483
|
+
| `Layer.mock(tag, implementation)` | Create layer with mock (partial for ServiceTag) |
|
|
484
|
+
| `Layer.create({ requires, apply })` | Create custom layer |
|
|
485
|
+
| `Layer.empty()` | Create empty layer |
|
|
486
|
+
| `Layer.merge(a, b)` | Merge two layers |
|
|
487
|
+
| `Layer.mergeAll(...layers)` | Merge multiple layers |
|
|
488
|
+
| `layer.provide(dep)` | Satisfy dependencies |
|
|
489
|
+
| `layer.provideMerge(dep)` | Satisfy and merge provisions |
|
|
490
|
+
| `layer.merge(other)` | Merge with another layer |
|
|
436
491
|
|
|
437
492
|
### ScopedContainer
|
|
438
493
|
|
|
439
|
-
| Method
|
|
440
|
-
|
|
441
|
-
| `ScopedContainer.builder(scope)`
|
|
442
|
-
| `ScopedContainer.empty(scope)`
|
|
443
|
-
| `ScopedContainer.from(scope, layer)` | Create from layer
|
|
444
|
-
| `container.child(scope)`
|
|
445
|
-
| `container.childFrom(scope, layer)`
|
|
494
|
+
| Method | Description |
|
|
495
|
+
| ------------------------------------ | ------------------------------------------- |
|
|
496
|
+
| `ScopedContainer.builder(scope)` | Create a new scoped container builder |
|
|
497
|
+
| `ScopedContainer.empty(scope)` | Create empty scoped container |
|
|
498
|
+
| `ScopedContainer.from(scope, layer)` | Create from layer |
|
|
499
|
+
| `container.child(scope)` | Create child scope builder |
|
|
500
|
+
| `container.childFrom(scope, layer)` | Create child scope from layer (convenience) |
|
|
446
501
|
|
|
447
502
|
### Tag
|
|
448
503
|
|
|
449
|
-
| Method
|
|
450
|
-
|
|
451
|
-
| `Tag.of(id)<T>()`
|
|
452
|
-
| `Tag.id(tag)`
|
|
453
|
-
| `Tag.isTag(value)` | Check if value is a tag
|
|
504
|
+
| Method | Description |
|
|
505
|
+
| ------------------ | --------------------------- |
|
|
506
|
+
| `Tag.of(id)<T>()` | Create a ValueTag |
|
|
507
|
+
| `Tag.id(tag)` | Get tag's string identifier |
|
|
508
|
+
| `Tag.isTag(value)` | Check if value is a tag |
|
|
509
|
+
|
|
510
|
+
## Testing
|
|
511
|
+
|
|
512
|
+
Sandly makes testing easy with `Layer.mock()`, which allows you to create partial mocks without satisfying constructor dependencies. Import your production layers and override dependencies with mocks:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// Production code (e.g., src/services/user-service.ts)
|
|
516
|
+
import { Layer } from 'sandly';
|
|
517
|
+
import { ResourcesRepository } from '../repositories/resources-repository';
|
|
518
|
+
|
|
519
|
+
export class UserService {
|
|
520
|
+
constructor(private repo: ResourcesRepository) {}
|
|
521
|
+
async getUsers() {
|
|
522
|
+
return this.repo.listByCrawlId('crawl-123');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Layer definition in the same file
|
|
527
|
+
export const userServiceLayer = Layer.service(UserService, [
|
|
528
|
+
ResourcesRepository,
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
// Test file (e.g., src/services/user-service.test.ts)
|
|
532
|
+
import { Container, Layer } from 'sandly';
|
|
533
|
+
import { userServiceLayer } from './user-service';
|
|
534
|
+
import { ResourcesRepository } from '../repositories/resources-repository';
|
|
535
|
+
|
|
536
|
+
// Override production dependencies with mocks
|
|
537
|
+
const testLayer = userServiceLayer.provide(
|
|
538
|
+
Layer.mock(ResourcesRepository, {
|
|
539
|
+
listByCrawlId: async () => [
|
|
540
|
+
{ id: '1', name: 'Alice' },
|
|
541
|
+
{ id: '2', name: 'Bob' },
|
|
542
|
+
],
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const container = Container.from(testLayer);
|
|
547
|
+
const userService = await container.resolve(UserService);
|
|
548
|
+
|
|
549
|
+
// Use the service - mock is automatically injected
|
|
550
|
+
const users = await userService.getUsers();
|
|
551
|
+
expect(users).toHaveLength(2);
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Benefits:**
|
|
555
|
+
|
|
556
|
+
- ✅ No need to satisfy constructor dependencies for mocks
|
|
557
|
+
- ✅ TypeScript validates mock method signatures
|
|
558
|
+
- ✅ Works seamlessly with `Layer.service()` composition
|
|
559
|
+
- ✅ Clear intent: `mock()` for tests, `value()` for production
|
|
454
560
|
|
|
455
561
|
## Comparison with Alternatives
|
|
456
562
|
|
|
457
|
-
| Feature
|
|
458
|
-
|
|
459
|
-
| Compile-time type safety
|
|
460
|
-
| No experimental decorators | ✅
|
|
461
|
-
| Async factories
|
|
462
|
-
| Framework-agnostic
|
|
463
|
-
| Layer composition
|
|
464
|
-
| Zero dependencies
|
|
563
|
+
| Feature | Sandly | NestJS | InversifyJS | TSyringe |
|
|
564
|
+
| -------------------------- | ------ | ------ | ----------- | -------- |
|
|
565
|
+
| Compile-time type safety | ✅ | ❌ | ⚠️ Partial | ❌ |
|
|
566
|
+
| No experimental decorators | ✅ | ❌ | ❌ | ❌ |
|
|
567
|
+
| Async factories | ✅ | ✅ | ❌ | ❌ |
|
|
568
|
+
| Framework-agnostic | ✅ | ❌ | ✅ | ✅ |
|
|
569
|
+
| Layer composition | ✅ | ❌ | ❌ | ❌ |
|
|
570
|
+
| Zero dependencies | ✅ | ❌ | ❌ | ❌ |
|
|
465
571
|
|
|
466
572
|
**Choose Sandly when you want:**
|
|
573
|
+
|
|
467
574
|
- Type safety without sacrificing simplicity
|
|
468
575
|
- DI without experimental decorators
|
|
469
576
|
- Composable, reusable dependency modules
|
package/dist/index.d.ts
CHANGED
|
@@ -325,12 +325,14 @@ declare const Layer: {
|
|
|
325
325
|
cleanup?: Finalizer<InstanceType<TClass>>;
|
|
326
326
|
}): Layer<ExtractTags<TDeps>, TClass>;
|
|
327
327
|
/**
|
|
328
|
-
* Creates a layer that provides a constant value.
|
|
328
|
+
* Creates a layer that provides a constant value or pre-instantiated instance.
|
|
329
329
|
*
|
|
330
|
-
*
|
|
331
|
-
* @param value - The value to provide
|
|
330
|
+
* Works with both ValueTags (for constants) and ServiceTags (for pre-instantiated instances, useful in tests).
|
|
332
331
|
*
|
|
333
|
-
* @
|
|
332
|
+
* @param tag - The tag (ValueTag or ServiceTag) to register
|
|
333
|
+
* @param value - The value or instance to provide
|
|
334
|
+
*
|
|
335
|
+
* @example ValueTag (constant)
|
|
334
336
|
* ```typescript
|
|
335
337
|
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
336
338
|
* const ConfigTag = Tag.of('config')<{ port: number }>();
|
|
@@ -338,8 +340,58 @@ declare const Layer: {
|
|
|
338
340
|
* const configLayer = Layer.value(ApiKeyTag, 'secret-key')
|
|
339
341
|
* .merge(Layer.value(ConfigTag, { port: 3000 }));
|
|
340
342
|
* ```
|
|
343
|
+
*
|
|
344
|
+
* @example ServiceTag (pre-instantiated instance, useful for testing)
|
|
345
|
+
* ```typescript
|
|
346
|
+
* class UserService {
|
|
347
|
+
* getUsers() { return []; }
|
|
348
|
+
* }
|
|
349
|
+
*
|
|
350
|
+
* const mockUserService = new UserService();
|
|
351
|
+
* const testLayer = Layer.value(UserService, mockUserService);
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
value<T extends AnyTag>(tag: T, value: TagType<T>): Layer<never, T>;
|
|
355
|
+
/**
|
|
356
|
+
* Creates a layer with a mock implementation for testing.
|
|
357
|
+
*
|
|
358
|
+
* Similar to `Layer.value()`, but allows partial implementations for ServiceTags,
|
|
359
|
+
* making it easier to create test mocks without satisfying constructor dependencies.
|
|
360
|
+
*
|
|
361
|
+
* **Use this for testing only.** For production code, use `Layer.value()` or `Layer.service()`.
|
|
362
|
+
*
|
|
363
|
+
* @param tag - The tag (ServiceTag or ValueTag) to register
|
|
364
|
+
* @param implementation - The mock implementation (can be partial for ServiceTags)
|
|
365
|
+
*
|
|
366
|
+
* @example ServiceTag with partial mock
|
|
367
|
+
* ```typescript
|
|
368
|
+
* class UserService {
|
|
369
|
+
* constructor(private db: Database) {}
|
|
370
|
+
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
371
|
+
* }
|
|
372
|
+
*
|
|
373
|
+
* // Mock only the methods you need - no need to satisfy constructor
|
|
374
|
+
* const testLayer = Layer.mock(UserService, {
|
|
375
|
+
* getUsers: () => Promise.resolve([{ id: 1, name: 'Alice' }])
|
|
376
|
+
* });
|
|
377
|
+
* ```
|
|
378
|
+
*
|
|
379
|
+
* @example ServiceTag with full mock instance
|
|
380
|
+
* ```typescript
|
|
381
|
+
* const mockUserService = {
|
|
382
|
+
* getUsers: () => Promise.resolve([])
|
|
383
|
+
* } as UserService;
|
|
384
|
+
*
|
|
385
|
+
* const testLayer = Layer.mock(UserService, mockUserService);
|
|
386
|
+
* ```
|
|
387
|
+
*
|
|
388
|
+
* @example ValueTag (works same as Layer.value)
|
|
389
|
+
* ```typescript
|
|
390
|
+
* const ConfigTag = Tag.of('config')<{ port: number }>();
|
|
391
|
+
* const testLayer = Layer.mock(ConfigTag, { port: 3000 });
|
|
392
|
+
* ```
|
|
341
393
|
*/
|
|
342
|
-
|
|
394
|
+
mock<T extends AnyTag>(tag: T, implementation: T extends ServiceTag ? Partial<TagType<T>> | TagType<T> : TagType<T>): Layer<never, T>;
|
|
343
395
|
/**
|
|
344
396
|
* Creates a custom layer with full control over the factory logic.
|
|
345
397
|
*
|
package/dist/index.js
CHANGED
|
@@ -894,6 +894,11 @@ const Layer = {
|
|
|
894
894
|
return builder.add(tag, () => value);
|
|
895
895
|
});
|
|
896
896
|
},
|
|
897
|
+
mock(tag, implementation) {
|
|
898
|
+
return createLayer((builder) => {
|
|
899
|
+
return builder.add(tag, () => implementation);
|
|
900
|
+
});
|
|
901
|
+
},
|
|
897
902
|
create(options) {
|
|
898
903
|
const layer = {
|
|
899
904
|
apply: (builder) => {
|