@bluelibs/runner 3.3.2 → 3.4.1
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 +491 -74
- package/dist/define.d.ts +5 -5
- package/dist/define.js +22 -2
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +55 -21
- package/dist/defs.js.map +1 -1
- package/dist/defs.returnTag.d.ts +36 -0
- package/dist/defs.returnTag.js +4 -0
- package/dist/defs.returnTag.js.map +1 -0
- package/dist/errors.d.ts +60 -10
- package/dist/errors.js +103 -12
- package/dist/errors.js.map +1 -1
- package/dist/globals/globalMiddleware.d.ts +4 -4
- package/dist/globals/globalResources.d.ts +28 -10
- package/dist/globals/middleware/cache.middleware.d.ts +9 -9
- package/dist/globals/resources/queue.resource.d.ts +5 -2
- package/dist/index.d.ts +33 -14
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/models/DependencyProcessor.js +4 -4
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.js +10 -1
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/Logger.d.ts +8 -0
- package/dist/models/Logger.js +24 -0
- package/dist/models/Logger.js.map +1 -1
- package/dist/models/OverrideManager.js +1 -1
- package/dist/models/OverrideManager.js.map +1 -1
- package/dist/models/ResourceInitializer.d.ts +2 -2
- package/dist/models/ResourceInitializer.js.map +1 -1
- package/dist/models/Store.d.ts +5 -3
- package/dist/models/Store.js +7 -1
- package/dist/models/Store.js.map +1 -1
- package/dist/models/StoreConstants.d.ts +6 -3
- package/dist/models/StoreRegistry.d.ts +5 -3
- package/dist/models/StoreRegistry.js +17 -1
- package/dist/models/StoreRegistry.js.map +1 -1
- package/dist/models/StoreTypes.d.ts +1 -1
- package/dist/models/StoreValidator.js +5 -5
- package/dist/models/StoreValidator.js.map +1 -1
- package/dist/models/TaskRunner.js +10 -0
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/run.d.ts +3 -3
- package/dist/run.js +1 -1
- package/dist/run.js.map +1 -1
- package/dist/t1.d.ts +1 -0
- package/dist/t1.js +13 -0
- package/dist/t1.js.map +1 -0
- package/dist/testing.d.ts +1 -1
- package/package.json +2 -2
- package/src/__tests__/errors.test.ts +92 -11
- package/src/__tests__/models/EventManager.test.ts +0 -1
- package/src/__tests__/models/Logger.test.ts +82 -5
- package/src/__tests__/models/Store.test.ts +57 -0
- package/src/__tests__/recursion/c.resource.ts +1 -1
- package/src/__tests__/run.overrides.test.ts +3 -3
- package/src/__tests__/typesafety.test.ts +112 -9
- package/src/__tests__/validation-edge-cases.test.ts +111 -0
- package/src/__tests__/validation-interface.test.ts +428 -0
- package/src/define.ts +47 -15
- package/src/defs.returnTag.ts +91 -0
- package/src/defs.ts +84 -27
- package/src/errors.ts +95 -23
- package/src/index.ts +1 -0
- package/src/models/DependencyProcessor.ts +9 -5
- package/src/models/EventManager.ts +12 -3
- package/src/models/Logger.ts +28 -0
- package/src/models/OverrideManager.ts +2 -7
- package/src/models/ResourceInitializer.ts +8 -3
- package/src/models/Store.ts +12 -3
- package/src/models/StoreRegistry.ts +27 -2
- package/src/models/StoreTypes.ts +1 -1
- package/src/models/StoreValidator.ts +6 -6
- package/src/models/TaskRunner.ts +10 -1
- package/src/run.ts +8 -5
- package/src/testing.ts +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ _Or: How I Learned to Stop Worrying and Love Dependency Injection_
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://github.com/bluelibs/runner/actions/workflows/ci.yml"><img src="https://github.com/bluelibs/runner/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status" /></a>
|
|
7
|
-
<a href="https://
|
|
7
|
+
<a href="https://github.com/bluelibs/runner"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen" alt="Coverage 100% is enforced. Code does not build without 100% on all branches, lines, etc." /></a>
|
|
8
8
|
<a href="https://bluelibs.github.io/runner/" target="_blank"><img src="https://img.shields.io/badge/read-typedocs-blue" alt="Docs" /></a>
|
|
9
9
|
<a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
|
|
10
10
|
</p>
|
|
@@ -601,6 +601,58 @@ The retry middleware can be configured with:
|
|
|
601
601
|
- `delayStrategy`: A function that returns the delay in milliseconds before the next attempt.
|
|
602
602
|
- `stopRetryIf`: A function to prevent retries for certain types of errors.
|
|
603
603
|
|
|
604
|
+
## Timeout Middleware: Never Wait Forever
|
|
605
|
+
|
|
606
|
+
The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
|
|
607
|
+
timeout:
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
import { globals } from "@bluelibs/runner";
|
|
611
|
+
|
|
612
|
+
const apiTask = task({
|
|
613
|
+
id: "app.tasks.externalApi",
|
|
614
|
+
middleware: [
|
|
615
|
+
globals.middleware.timeout.with({ ttl: 5000 }), // 5 second timeout
|
|
616
|
+
],
|
|
617
|
+
run: async () => {
|
|
618
|
+
// This operation will be aborted if it takes longer than 5 seconds
|
|
619
|
+
return await fetch("https://slow-api.example.com/data");
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Combine with retry for robust error handling
|
|
624
|
+
const resilientTask = task({
|
|
625
|
+
id: "app.tasks.resilient",
|
|
626
|
+
middleware: [
|
|
627
|
+
// Order matters here. Imagine a big onion.
|
|
628
|
+
globals.middleware.retry.with({
|
|
629
|
+
retries: 3,
|
|
630
|
+
delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
|
|
631
|
+
}),
|
|
632
|
+
globals.middleware.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
|
|
633
|
+
],
|
|
634
|
+
run: async () => {
|
|
635
|
+
// Each retry attempt gets its own 10-second timeout
|
|
636
|
+
return await unreliableOperation();
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
How it works:
|
|
642
|
+
|
|
643
|
+
- Uses AbortController and Promise.race() for clean cancellation
|
|
644
|
+
- Throws TimeoutError when the timeout is reached
|
|
645
|
+
- Works with any async operation in tasks and resources
|
|
646
|
+
- Integrates seamlessly with retry middleware for layered resilience
|
|
647
|
+
- Zero timeout (ttl: 0) throws immediately for testing edge cases
|
|
648
|
+
|
|
649
|
+
Best practices:
|
|
650
|
+
|
|
651
|
+
- Set timeouts based on expected operation duration plus buffer
|
|
652
|
+
- Combine with retry middleware for transient failures
|
|
653
|
+
- Use longer timeouts for resource initialization than task execution
|
|
654
|
+
- Consider network conditions when setting API call timeouts
|
|
655
|
+
|
|
604
656
|
## Logging: Because Console.log Isn't Professional
|
|
605
657
|
|
|
606
658
|
_The structured logging system that actually makes debugging enjoyable_
|
|
@@ -616,18 +668,32 @@ const businessTask = task({
|
|
|
616
668
|
id: "app.tasks.business",
|
|
617
669
|
dependencies: { logger: globals.resources.logger },
|
|
618
670
|
run: async (_, { logger }) => {
|
|
619
|
-
logger.info("Starting business process");
|
|
620
|
-
logger.warn("This might take a while");
|
|
671
|
+
logger.info("Starting business process"); // ✅ Visible by default
|
|
672
|
+
logger.warn("This might take a while"); // ✅ Visible by default
|
|
621
673
|
logger.error("Oops, something went wrong", {
|
|
674
|
+
// ✅ Visible by default
|
|
622
675
|
error: new Error("Database connection failed"),
|
|
623
676
|
});
|
|
624
677
|
logger.critical("System is on fire", {
|
|
678
|
+
// ✅ Visible by default
|
|
625
679
|
data: { temperature: "9000°C" },
|
|
626
680
|
});
|
|
681
|
+
logger.debug("Debug information"); // ❌ Hidden by default
|
|
682
|
+
logger.trace("Very detailed trace"); // ❌ Hidden by default
|
|
627
683
|
},
|
|
628
684
|
});
|
|
629
685
|
```
|
|
630
686
|
|
|
687
|
+
**Good news!** Logs at `info` level and above are visible by default, so you'll see your application logs immediately without any configuration. For development and debugging, you can easily show more detailed logs:
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
# Show debug logs and framework internals
|
|
691
|
+
RUNNER_LOG_LEVEL=debug node your-app.js
|
|
692
|
+
|
|
693
|
+
# Hide all logs for production
|
|
694
|
+
RUNNER_DISABLE_LOGS=true node your-app.js
|
|
695
|
+
```
|
|
696
|
+
|
|
631
697
|
### Log Levels: From Whispers to Screams
|
|
632
698
|
|
|
633
699
|
The logger supports six log levels with increasing severity:
|
|
@@ -733,18 +799,31 @@ const requestHandler = task({
|
|
|
733
799
|
|
|
734
800
|
### Print Threshold: Control What Shows Up
|
|
735
801
|
|
|
736
|
-
By default, logs
|
|
802
|
+
By default, logs at `info` level and above are automatically printed to console for better developer experience. You can easily control this behavior through environment variables or by setting a print threshold programmatically:
|
|
803
|
+
|
|
804
|
+
#### Environment Variable Controls
|
|
805
|
+
|
|
806
|
+
```bash
|
|
807
|
+
# Disable all logging output
|
|
808
|
+
RUNNER_DISABLE_LOGS=true node your-app.js
|
|
809
|
+
|
|
810
|
+
# Set specific log level (trace, debug, info, warn, error, critical)
|
|
811
|
+
RUNNER_LOG_LEVEL=debug node your-app.js
|
|
812
|
+
RUNNER_LOG_LEVEL=error node your-app.js
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
#### Programmatic Control
|
|
737
816
|
|
|
738
817
|
```typescript
|
|
739
|
-
//
|
|
818
|
+
// Override the default print threshold programmatically
|
|
740
819
|
const setupLogging = task({
|
|
741
820
|
id: "app.logging.setup",
|
|
742
821
|
on: globals.resources.logger.events.afterInit,
|
|
743
822
|
run: async (event) => {
|
|
744
823
|
const logger = event.data.value;
|
|
745
824
|
|
|
746
|
-
// Print
|
|
747
|
-
logger.setPrintThreshold("
|
|
825
|
+
// Print debug level and above (debug, info, warn, error, critical)
|
|
826
|
+
logger.setPrintThreshold("debug");
|
|
748
827
|
|
|
749
828
|
// Print only errors and critical issues
|
|
750
829
|
logger.setPrintThreshold("error");
|
|
@@ -1164,6 +1243,8 @@ const apiEndpoint = task({
|
|
|
1164
1243
|
});
|
|
1165
1244
|
```
|
|
1166
1245
|
|
|
1246
|
+
To process these tags you can hook into `globals.events.afterInit`, use the global store as dependency and use the `getTasksWithTag()` and `getResourcesWithTag()` functionality.
|
|
1247
|
+
|
|
1167
1248
|
#### Smart Middleware Using Structured Tags
|
|
1168
1249
|
|
|
1169
1250
|
```typescript
|
|
@@ -1171,16 +1252,16 @@ const performanceMiddleware = middleware({
|
|
|
1171
1252
|
id: "app.middleware.performance",
|
|
1172
1253
|
run: async ({ task, next }) => {
|
|
1173
1254
|
const tags = task.definition.meta?.tags || [];
|
|
1174
|
-
const
|
|
1255
|
+
const perfConfigTag = performanceTag.extract(tags); // or easier: .extract(task.definition)
|
|
1175
1256
|
|
|
1176
|
-
if (
|
|
1257
|
+
if (perfConfigTag) {
|
|
1177
1258
|
const startTime = Date.now();
|
|
1178
1259
|
|
|
1179
1260
|
try {
|
|
1180
1261
|
const result = await next(task.input);
|
|
1181
1262
|
const duration = Date.now() - startTime;
|
|
1182
1263
|
|
|
1183
|
-
if (duration >
|
|
1264
|
+
if (duration > perfConfigTag.config.criticalAboveMs) {
|
|
1184
1265
|
await alerting.critical(
|
|
1185
1266
|
`Task ${task.definition.id} took ${duration}ms`
|
|
1186
1267
|
);
|
|
@@ -1202,38 +1283,74 @@ const performanceMiddleware = middleware({
|
|
|
1202
1283
|
return next(task.input);
|
|
1203
1284
|
},
|
|
1204
1285
|
});
|
|
1286
|
+
```
|
|
1205
1287
|
|
|
1206
|
-
|
|
1207
|
-
id: "app.middleware.rateLimit",
|
|
1208
|
-
dependencies: { redis },
|
|
1209
|
-
run: async ({ task, next }, { redis }) => {
|
|
1210
|
-
// Extraction can be done at task.definition level or at task.definition.meta.tags
|
|
1211
|
-
const rateLimitCurrentTag = rateLimitTag.extract(task.definition);
|
|
1288
|
+
#### Contract Tags: Enforcing Return Types
|
|
1212
1289
|
|
|
1213
|
-
|
|
1214
|
-
const tags = task.definition.meta?.tags;
|
|
1215
|
-
const rateLimitCurrentTag = rateLimitTag.extract(tags);
|
|
1290
|
+
You can attach contracts to tags to enforce the shape of a task's returned value and a resource's `init()` value at compile time. Contracts are specified via the second generic of `defineTag<TConfig, TContract>`.
|
|
1216
1291
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1292
|
+
```typescript
|
|
1293
|
+
// A tag that enforces the returned value to include { name: string }
|
|
1294
|
+
const userContract = tag<void, { name: string }>({ id: "contract.user" });
|
|
1220
1295
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}
|
|
1296
|
+
// Another tag that enforces { age: number }
|
|
1297
|
+
const ageContract = tag<void, { age: number }>({ id: "contract.age" });
|
|
1224
1298
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1299
|
+
// Works with configured tags too
|
|
1300
|
+
const preferenceContract = tag<{ locale: string }, { preferredLocale: string }>(
|
|
1301
|
+
{ id: "contract.preferences" }
|
|
1302
|
+
);
|
|
1303
|
+
```
|
|
1229
1304
|
|
|
1230
|
-
|
|
1305
|
+
When these tags are present in `meta.tags`, the returned value must satisfy the intersection of all contract types:
|
|
1306
|
+
|
|
1307
|
+
```typescript
|
|
1308
|
+
// Task: the awaited return value must satisfy { name: string } & { age: number }
|
|
1309
|
+
const getProfile = task({
|
|
1310
|
+
id: "app.tasks.getProfile",
|
|
1311
|
+
meta: {
|
|
1312
|
+
tags: [
|
|
1313
|
+
userContract,
|
|
1314
|
+
ageContract,
|
|
1315
|
+
preferenceContract.with({ locale: "en" }),
|
|
1316
|
+
],
|
|
1317
|
+
},
|
|
1318
|
+
run: async () => {
|
|
1319
|
+
return { name: "Ada", age: 37, preferredLocale: "en" }; // OK
|
|
1231
1320
|
},
|
|
1232
1321
|
});
|
|
1322
|
+
|
|
1323
|
+
// Resource: init() return must satisfy the same intersection
|
|
1324
|
+
const profileService = resource({
|
|
1325
|
+
id: "app.resources.profileService",
|
|
1326
|
+
meta: { tags: [userContract, ageContract] },
|
|
1327
|
+
init: async () => {
|
|
1328
|
+
return { name: "Ada", age: 37 }; // OK
|
|
1329
|
+
},
|
|
1330
|
+
});
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
If the returned value does not satisfy the intersection, TypeScript surfaces a readable, verbose type error that includes what was expected and what was received.
|
|
1334
|
+
|
|
1335
|
+
```typescript
|
|
1336
|
+
const badTask = task({
|
|
1337
|
+
id: "app.tasks.bad",
|
|
1338
|
+
meta: { tags: [userContract, ageContract] },
|
|
1339
|
+
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
|
1340
|
+
run: async () => ({ name: "Ada" }), // Missing { age: number }
|
|
1341
|
+
// Type error includes a helpful shape similar to:
|
|
1342
|
+
// ContractViolationError<
|
|
1343
|
+
// { message: "Value does not satisfy all tag contracts";
|
|
1344
|
+
// expected: { name: string } & { age: number };
|
|
1345
|
+
// received: { name: string } }
|
|
1346
|
+
// >
|
|
1347
|
+
});
|
|
1233
1348
|
```
|
|
1234
1349
|
|
|
1235
1350
|
### When to Use Metadata
|
|
1236
1351
|
|
|
1352
|
+
Always strive to provide a title and a description.
|
|
1353
|
+
|
|
1237
1354
|
#### ✅ Great Use Cases
|
|
1238
1355
|
|
|
1239
1356
|
**Documentation & Discovery**
|
|
@@ -1284,33 +1401,6 @@ const developmentTask = task({
|
|
|
1284
1401
|
});
|
|
1285
1402
|
```
|
|
1286
1403
|
|
|
1287
|
-
#### ❌ When NOT to Use Metadata
|
|
1288
|
-
|
|
1289
|
-
**Simple Internal Logic** - Don't overcomplicate straightforward code:
|
|
1290
|
-
|
|
1291
|
-
```typescript
|
|
1292
|
-
// ❌ Overkill
|
|
1293
|
-
const simple = task({
|
|
1294
|
-
meta: { tags: ["internal", "utility"] },
|
|
1295
|
-
run: () => Math.random(),
|
|
1296
|
-
});
|
|
1297
|
-
|
|
1298
|
-
// ✅ Better
|
|
1299
|
-
const generateId = () => Math.random().toString(36);
|
|
1300
|
-
```
|
|
1301
|
-
|
|
1302
|
-
**One-Off Tasks** - If it's used once, metadata won't help:
|
|
1303
|
-
|
|
1304
|
-
```typescript
|
|
1305
|
-
// ❌ Unnecessary
|
|
1306
|
-
const oneTimeScript = task({
|
|
1307
|
-
meta: { title: "Migration Script", tags: ["migration"] },
|
|
1308
|
-
run: () => {
|
|
1309
|
-
/* run once and forget */
|
|
1310
|
-
},
|
|
1311
|
-
});
|
|
1312
|
-
```
|
|
1313
|
-
|
|
1314
1404
|
### Extending Metadata: Custom Properties
|
|
1315
1405
|
|
|
1316
1406
|
For advanced use cases, you can extend the metadata interfaces to add your own properties:
|
|
@@ -1365,23 +1455,6 @@ const database = resource({
|
|
|
1365
1455
|
|
|
1366
1456
|
### Advanced Patterns
|
|
1367
1457
|
|
|
1368
|
-
#### Tag-Based Component Selection
|
|
1369
|
-
|
|
1370
|
-
```typescript
|
|
1371
|
-
// Find all API endpoints
|
|
1372
|
-
function getApiTasks(store: Store) {
|
|
1373
|
-
return store.getAllTasks().filter((task) => task.meta?.tags?.includes("api"));
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// Find all tasks with specific performance requirements
|
|
1377
|
-
function getPerformanceCriticalTasks(store: Store) {
|
|
1378
|
-
return store.getAllTasks().filter((task) => {
|
|
1379
|
-
const tags = task.meta?.tags || [];
|
|
1380
|
-
return performanceTag.extract(tags) !== null;
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
```
|
|
1384
|
-
|
|
1385
1458
|
#### Dynamic Middleware Application
|
|
1386
1459
|
|
|
1387
1460
|
```typescript
|
|
@@ -1512,6 +1585,350 @@ const app = resource({
|
|
|
1512
1585
|
});
|
|
1513
1586
|
```
|
|
1514
1587
|
|
|
1588
|
+
### Runtime Input and Config Validation
|
|
1589
|
+
|
|
1590
|
+
BlueLibs Runner includes a generic validation interface that works with any validation library, including [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), [Joi](https://joi.dev/), and others. The framework provides runtime validation with excellent TypeScript inference while remaining library-agnostic.
|
|
1591
|
+
|
|
1592
|
+
#### The Validation Interface
|
|
1593
|
+
|
|
1594
|
+
The framework defines a simple `IValidationSchema<T>` interface that any validation library can implement:
|
|
1595
|
+
|
|
1596
|
+
```typescript
|
|
1597
|
+
interface IValidationSchema<T> {
|
|
1598
|
+
parse(input: unknown): T;
|
|
1599
|
+
}
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
Popular validation libraries already implement this interface:
|
|
1603
|
+
|
|
1604
|
+
- **Zod**: `.parse()` method works directly
|
|
1605
|
+
- **Yup**: Use `.validateSync()` or create a wrapper
|
|
1606
|
+
- **Joi**: Use `.assert()` or create a wrapper
|
|
1607
|
+
- **Custom validators**: Implement the interface yourself
|
|
1608
|
+
|
|
1609
|
+
#### Task Input Validation
|
|
1610
|
+
|
|
1611
|
+
Add an `inputSchema` to any task to validate inputs before execution:
|
|
1612
|
+
|
|
1613
|
+
```typescript
|
|
1614
|
+
import { z } from "zod";
|
|
1615
|
+
import { task, resource, run } from "@bluelibs/runner";
|
|
1616
|
+
|
|
1617
|
+
const userSchema = z.object({
|
|
1618
|
+
name: z.string().min(2),
|
|
1619
|
+
email: z.string().email(),
|
|
1620
|
+
age: z.number().min(0).max(150),
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
const createUserTask = task({
|
|
1624
|
+
id: "app.tasks.createUser",
|
|
1625
|
+
inputSchema: userSchema, // Works directly with Zod!
|
|
1626
|
+
run: async (userData) => {
|
|
1627
|
+
// userData is validated and properly typed
|
|
1628
|
+
return { id: "user-123", ...userData };
|
|
1629
|
+
},
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
const app = resource({
|
|
1633
|
+
id: "app",
|
|
1634
|
+
register: [createUserTask],
|
|
1635
|
+
dependencies: { createUserTask },
|
|
1636
|
+
init: async (_, { createUserTask }) => {
|
|
1637
|
+
// This works - valid input
|
|
1638
|
+
const user = await createUserTask({
|
|
1639
|
+
name: "John Doe",
|
|
1640
|
+
email: "john@example.com",
|
|
1641
|
+
age: 30,
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// This throws a validation error at runtime
|
|
1645
|
+
try {
|
|
1646
|
+
await createUserTask({
|
|
1647
|
+
name: "J", // Too short
|
|
1648
|
+
email: "invalid-email", // Invalid format
|
|
1649
|
+
age: -5, // Negative age
|
|
1650
|
+
});
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
console.log(error.message);
|
|
1653
|
+
// "Task input validation failed for app.tasks.createUser: ..."
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
#### Resource Config Validation (Fail Fast)
|
|
1660
|
+
|
|
1661
|
+
Add a `configSchema` to resources to validate configurations. **Validation happens immediately when `.with()` is called**, ensuring configuration errors are caught early:
|
|
1662
|
+
|
|
1663
|
+
```typescript
|
|
1664
|
+
const databaseConfigSchema = z.object({
|
|
1665
|
+
host: z.string(),
|
|
1666
|
+
port: z.number().min(1).max(65535),
|
|
1667
|
+
database: z.string(),
|
|
1668
|
+
ssl: z.boolean().default(false), // Optional with default
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
const databaseResource = resource({
|
|
1672
|
+
id: "app.resources.database",
|
|
1673
|
+
configSchema: databaseConfigSchema, // Validation on .with()
|
|
1674
|
+
init: async (config) => {
|
|
1675
|
+
// config is already validated and has proper types
|
|
1676
|
+
return createConnection({
|
|
1677
|
+
host: config.host,
|
|
1678
|
+
port: config.port,
|
|
1679
|
+
database: config.database,
|
|
1680
|
+
ssl: config.ssl,
|
|
1681
|
+
});
|
|
1682
|
+
},
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
// Validation happens here, not during init!
|
|
1686
|
+
try {
|
|
1687
|
+
const configuredResource = databaseResource.with({
|
|
1688
|
+
host: "localhost",
|
|
1689
|
+
port: 99999, // Invalid: port too high
|
|
1690
|
+
database: "myapp",
|
|
1691
|
+
});
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
// "Resource config validation failed for app.resources.database: ..."
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const app = resource({
|
|
1697
|
+
id: "app",
|
|
1698
|
+
register: [
|
|
1699
|
+
databaseResource.with({
|
|
1700
|
+
host: "localhost",
|
|
1701
|
+
port: 5432,
|
|
1702
|
+
database: "myapp",
|
|
1703
|
+
// ssl defaults to false
|
|
1704
|
+
}),
|
|
1705
|
+
],
|
|
1706
|
+
});
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
#### Event Payload Validation
|
|
1710
|
+
|
|
1711
|
+
Add a `payloadSchema` to events to validate payloads every time they're emitted:
|
|
1712
|
+
|
|
1713
|
+
```typescript
|
|
1714
|
+
const userActionSchema = z.object({
|
|
1715
|
+
userId: z.string().uuid(),
|
|
1716
|
+
action: z.enum(["created", "updated", "deleted"]),
|
|
1717
|
+
timestamp: z.date().default(() => new Date()),
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
const userActionEvent = event({
|
|
1721
|
+
id: "app.events.userAction",
|
|
1722
|
+
payloadSchema: userActionSchema, // Validates on emit
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
const notificationTask = task({
|
|
1726
|
+
id: "app.tasks.sendNotification",
|
|
1727
|
+
on: userActionEvent,
|
|
1728
|
+
run: async (eventData) => {
|
|
1729
|
+
// eventData.data is validated and properly typed
|
|
1730
|
+
console.log(`User ${eventData.data.userId} was ${eventData.data.action}`);
|
|
1731
|
+
},
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
const app = resource({
|
|
1735
|
+
id: "app",
|
|
1736
|
+
register: [userActionEvent, notificationTask],
|
|
1737
|
+
dependencies: { userActionEvent },
|
|
1738
|
+
init: async (_, { userActionEvent }) => {
|
|
1739
|
+
// This works - valid payload
|
|
1740
|
+
await userActionEvent({
|
|
1741
|
+
userId: "123e4567-e89b-12d3-a456-426614174000",
|
|
1742
|
+
action: "created",
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
// This throws validation error when emitted
|
|
1746
|
+
try {
|
|
1747
|
+
await userActionEvent({
|
|
1748
|
+
userId: "invalid-uuid",
|
|
1749
|
+
action: "unknown",
|
|
1750
|
+
});
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
// "Event payload validation failed for app.events.userAction: ..."
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
```
|
|
1757
|
+
|
|
1758
|
+
#### Middleware Config Validation (Fail Fast)
|
|
1759
|
+
|
|
1760
|
+
Add a `configSchema` to middleware to validate configurations. Like resources, **validation happens immediately when `.with()` is called**:
|
|
1761
|
+
|
|
1762
|
+
```typescript
|
|
1763
|
+
const timingConfigSchema = z.object({
|
|
1764
|
+
timeout: z.number().positive(),
|
|
1765
|
+
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
1766
|
+
logSuccessful: z.boolean().default(true),
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
const timingMiddleware = middleware({
|
|
1770
|
+
id: "app.middleware.timing",
|
|
1771
|
+
configSchema: timingConfigSchema, // Validation on .with()
|
|
1772
|
+
run: async ({ next }, _, config) => {
|
|
1773
|
+
const start = Date.now();
|
|
1774
|
+
try {
|
|
1775
|
+
const result = await next();
|
|
1776
|
+
const duration = Date.now() - start;
|
|
1777
|
+
if (config.logSuccessful && config.logLevel === "debug") {
|
|
1778
|
+
console.log(`Operation completed in ${duration}ms`);
|
|
1779
|
+
}
|
|
1780
|
+
return result;
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
const duration = Date.now() - start;
|
|
1783
|
+
console.log(`Operation failed after ${duration}ms`);
|
|
1784
|
+
throw error;
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
// Validation happens here, not during execution!
|
|
1790
|
+
try {
|
|
1791
|
+
const configuredMiddleware = timingMiddleware.with({
|
|
1792
|
+
timeout: -5, // Invalid: negative timeout
|
|
1793
|
+
logLevel: "invalid", // Invalid: not in enum
|
|
1794
|
+
});
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
// "Middleware config validation failed for app.middleware.timing: ..."
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const myTask = task({
|
|
1800
|
+
id: "app.tasks.example",
|
|
1801
|
+
middleware: [
|
|
1802
|
+
timingMiddleware.with({
|
|
1803
|
+
timeout: 5000,
|
|
1804
|
+
logLevel: "debug",
|
|
1805
|
+
logSuccessful: true,
|
|
1806
|
+
}),
|
|
1807
|
+
],
|
|
1808
|
+
run: async () => "success",
|
|
1809
|
+
});
|
|
1810
|
+
```
|
|
1811
|
+
|
|
1812
|
+
#### Advanced Validation Features
|
|
1813
|
+
|
|
1814
|
+
Any validation library features work with the generic interface. Here's an example with transformations and refinements:
|
|
1815
|
+
|
|
1816
|
+
```typescript
|
|
1817
|
+
const advancedSchema = z
|
|
1818
|
+
.object({
|
|
1819
|
+
userId: z.string().uuid(),
|
|
1820
|
+
amount: z.string().transform((val) => parseFloat(val)), // Transform string to number
|
|
1821
|
+
currency: z.enum(["USD", "EUR", "GBP"]),
|
|
1822
|
+
metadata: z.record(z.string()).optional(),
|
|
1823
|
+
})
|
|
1824
|
+
.refine((data) => data.amount > 0, {
|
|
1825
|
+
message: "Amount must be positive",
|
|
1826
|
+
path: ["amount"],
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
const paymentTask = task({
|
|
1830
|
+
id: "app.tasks.payment",
|
|
1831
|
+
inputSchema: advancedSchema,
|
|
1832
|
+
run: async (payment) => {
|
|
1833
|
+
// payment.amount is now a number (transformed from string)
|
|
1834
|
+
// All validations have passed
|
|
1835
|
+
return processPayment(payment);
|
|
1836
|
+
},
|
|
1837
|
+
});
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
#### Error Handling
|
|
1841
|
+
|
|
1842
|
+
Validation errors are thrown with clear, descriptive messages that include the component ID:
|
|
1843
|
+
|
|
1844
|
+
```typescript
|
|
1845
|
+
// Task validation error format:
|
|
1846
|
+
// "Task input validation failed for {taskId}: {validationErrorMessage}"
|
|
1847
|
+
|
|
1848
|
+
// Resource validation error format (thrown on .with() call):
|
|
1849
|
+
// "Resource config validation failed for {resourceId}: {validationErrorMessage}"
|
|
1850
|
+
|
|
1851
|
+
// Event validation error format (thrown on emit):
|
|
1852
|
+
// "Event payload validation failed for {eventId}: {validationErrorMessage}"
|
|
1853
|
+
|
|
1854
|
+
// Middleware validation error format (thrown on .with() call):
|
|
1855
|
+
// "Middleware config validation failed for {middlewareId}: {validationErrorMessage}"
|
|
1856
|
+
```
|
|
1857
|
+
|
|
1858
|
+
#### Using Different Validation Libraries
|
|
1859
|
+
|
|
1860
|
+
The framework works with any validation library that implements the `IValidationSchema<T>` interface:
|
|
1861
|
+
|
|
1862
|
+
```typescript
|
|
1863
|
+
// Zod (works directly)
|
|
1864
|
+
import { z } from "zod";
|
|
1865
|
+
const zodSchema = z.string().email();
|
|
1866
|
+
|
|
1867
|
+
// Yup (with wrapper)
|
|
1868
|
+
import * as yup from "yup";
|
|
1869
|
+
const yupSchema = {
|
|
1870
|
+
parse: (input: unknown) => yup.string().email().validateSync(input),
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
// Joi (with wrapper)
|
|
1874
|
+
import Joi from "joi";
|
|
1875
|
+
const joiSchema = {
|
|
1876
|
+
parse: (input: unknown) => {
|
|
1877
|
+
const { error, value } = Joi.string().email().validate(input);
|
|
1878
|
+
if (error) throw error;
|
|
1879
|
+
return value;
|
|
1880
|
+
},
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
// Custom validation
|
|
1884
|
+
const customSchema = {
|
|
1885
|
+
parse: (input: unknown) => {
|
|
1886
|
+
if (typeof input !== "string" || !input.includes("@")) {
|
|
1887
|
+
throw new Error("Must be a valid email");
|
|
1888
|
+
}
|
|
1889
|
+
return input;
|
|
1890
|
+
},
|
|
1891
|
+
};
|
|
1892
|
+
```
|
|
1893
|
+
|
|
1894
|
+
#### When to Use Validation
|
|
1895
|
+
|
|
1896
|
+
- **API boundaries**: Validating user inputs from HTTP requests
|
|
1897
|
+
- **External data**: Processing data from files, databases, or APIs
|
|
1898
|
+
- **Configuration**: Ensuring environment variables and configs are correct (fail fast)
|
|
1899
|
+
- **Event payloads**: Validating data in event-driven architectures
|
|
1900
|
+
- **Middleware configs**: Validating middleware settings at registration time (fail fast)
|
|
1901
|
+
|
|
1902
|
+
#### Performance Notes
|
|
1903
|
+
|
|
1904
|
+
- Validation only runs when schemas are provided (zero overhead when not used)
|
|
1905
|
+
- Resource and middleware validation happens once at registration time (`.with()`)
|
|
1906
|
+
- Task and event validation happens at runtime
|
|
1907
|
+
- Consider the validation library's performance characteristics for your use case
|
|
1908
|
+
- All major validation libraries are optimized for runtime validation
|
|
1909
|
+
|
|
1910
|
+
#### TypeScript Integration
|
|
1911
|
+
|
|
1912
|
+
While runtime validation happens with your chosen library, TypeScript still enforces compile-time types. For the best experience:
|
|
1913
|
+
|
|
1914
|
+
```typescript
|
|
1915
|
+
// With Zod, define your type and schema together
|
|
1916
|
+
type UserData = z.infer<typeof userSchema>;
|
|
1917
|
+
|
|
1918
|
+
const userSchema = z.object({
|
|
1919
|
+
name: z.string(),
|
|
1920
|
+
email: z.string().email(),
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
const createUser = task({
|
|
1924
|
+
inputSchema: userSchema,
|
|
1925
|
+
run: async (input: UserData) => {
|
|
1926
|
+
// Both runtime validation AND compile-time typing
|
|
1927
|
+
return { id: "user-123", ...input };
|
|
1928
|
+
},
|
|
1929
|
+
});
|
|
1930
|
+
```
|
|
1931
|
+
|
|
1515
1932
|
### Internal Services: For When You Need Direct Access
|
|
1516
1933
|
|
|
1517
1934
|
We expose the internal services for advanced use cases (but try not to use them unless you really need to):
|
package/dist/define.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* metadata: anonymous IDs, file path tags (for better debugging), lifecycle
|
|
6
6
|
* events, and global middleware flags. See README for high-level concepts.
|
|
7
7
|
*/
|
|
8
|
-
import { ITask, ITaskDefinition, IResource, IResourceWithConfig, IResourceDefinition, IEventDefinition, IMiddlewareDefinition, DependencyMapType, DependencyValuesType, IMiddleware, IEvent, RegisterableItems, ITag, ITagDefinition } from "./defs";
|
|
9
|
-
export declare function defineTask<Input = undefined, Output extends Promise<any> = any, Deps extends DependencyMapType = any, TOn extends "*" | IEventDefinition | undefined = undefined>(taskConfig: ITaskDefinition<Input, Output, Deps, TOn>): ITask<Input, Output, Deps, TOn>;
|
|
10
|
-
export declare function defineResource<TConfig = void, TValue = any
|
|
8
|
+
import { ITask, ITaskDefinition, IResource, IResourceWithConfig, IResourceDefinition, IEventDefinition, IMiddlewareDefinition, DependencyMapType, DependencyValuesType, IMiddleware, IEvent, RegisterableItems, ITag, ITagDefinition, ITaskMeta, IResourceMeta } from "./defs";
|
|
9
|
+
export declare function defineTask<Input = undefined, Output extends Promise<any> = any, Deps extends DependencyMapType = any, TOn extends "*" | IEventDefinition | undefined = undefined, TMeta extends ITaskMeta = any>(taskConfig: ITaskDefinition<Input, Output, Deps, TOn, TMeta>): ITask<Input, Output, Deps, TOn, TMeta>;
|
|
10
|
+
export declare function defineResource<TConfig = void, TValue extends Promise<any> = Promise<any>, TDeps extends DependencyMapType = {}, TPrivate = any, TMeta extends IResourceMeta = any>(constConfig: IResourceDefinition<TConfig, TValue, TDeps, TPrivate, any, any, TMeta>): IResource<TConfig, TValue, TDeps, TPrivate, TMeta>;
|
|
11
11
|
/**
|
|
12
12
|
* Creates an "index" resource which groups multiple registerable items under
|
|
13
13
|
* a single dependency. The resulting resource registers every item, depends
|
|
@@ -16,7 +16,7 @@ export declare function defineResource<TConfig = void, TValue = any, TDeps exten
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function defineIndex<T extends Record<string, RegisterableItems>, D extends {
|
|
18
18
|
[K in keyof T]: T[K] extends IResourceWithConfig<any, any, any> ? T[K]["resource"] : T[K];
|
|
19
|
-
} & DependencyMapType>(items: T): IResource<void, DependencyValuesType<D
|
|
19
|
+
} & DependencyMapType>(items: T): IResource<void, Promise<DependencyValuesType<D>>, D>;
|
|
20
20
|
export declare function defineEvent<TPayload = void>(config?: IEventDefinition<TPayload>): IEvent<TPayload>;
|
|
21
21
|
export type MiddlewareEverywhereOptions = {
|
|
22
22
|
/**
|
|
@@ -46,4 +46,4 @@ export declare function defineOverride<T extends IMiddleware<any, any>>(base: T,
|
|
|
46
46
|
* - `.with(config)` to create configured instances
|
|
47
47
|
* - `.extract(tags)` to extract this tag from a list of tags
|
|
48
48
|
*/
|
|
49
|
-
export declare function defineTag<TConfig = void>(definition: ITagDefinition<TConfig>): ITag<TConfig>;
|
|
49
|
+
export declare function defineTag<TConfig = void, TEnforceContract = void>(definition: ITagDefinition<TConfig, TEnforceContract>): ITag<TConfig, TEnforceContract>;
|