@bluelibs/runner 3.3.2 → 3.4.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 +437 -33
- 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 +2 -2
- package/dist/models/Store.js +1 -1
- package/dist/models/Store.js.map +1 -1
- package/dist/models/StoreConstants.d.ts +6 -3
- package/dist/models/StoreRegistry.d.ts +2 -2
- package/dist/models/StoreRegistry.js +1 -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__/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 +3 -3
- package/src/models/StoreRegistry.ts +2 -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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { defineTask, defineResource, defineEvent, defineMiddleware } from "../define";
|
|
2
|
+
import { run } from "../run";
|
|
3
|
+
import { IValidationSchema } from "../defs";
|
|
4
|
+
|
|
5
|
+
// Simple mock validation schemas for testing the interface
|
|
6
|
+
class MockValidationSchema<T> implements IValidationSchema<T> {
|
|
7
|
+
constructor(
|
|
8
|
+
private validator: (input: unknown) => T,
|
|
9
|
+
private errorMessage?: string
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
parse(input: unknown): T {
|
|
13
|
+
try {
|
|
14
|
+
return this.validator(input);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error(this.errorMessage || "Validation failed");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper functions to create mock schemas similar to Zod
|
|
22
|
+
const mockSchema = {
|
|
23
|
+
object: <T extends Record<string, any>>(shape: Record<keyof T, string>): IValidationSchema<T> => {
|
|
24
|
+
return new MockValidationSchema((input: unknown) => {
|
|
25
|
+
if (typeof input !== "object" || input === null) {
|
|
26
|
+
throw new Error("Expected object");
|
|
27
|
+
}
|
|
28
|
+
const obj = input as any;
|
|
29
|
+
for (const [key, expectedType] of Object.entries(shape)) {
|
|
30
|
+
if (expectedType === "string" && typeof obj[key] !== "string") {
|
|
31
|
+
throw new Error(`${key} must be string`);
|
|
32
|
+
}
|
|
33
|
+
if (expectedType === "number" && typeof obj[key] !== "number") {
|
|
34
|
+
throw new Error(`${key} must be number`);
|
|
35
|
+
}
|
|
36
|
+
if (expectedType === "boolean" && typeof obj[key] !== "boolean") {
|
|
37
|
+
throw new Error(`${key} must be boolean`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return obj as T;
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
string: (): IValidationSchema<string> => {
|
|
45
|
+
return new MockValidationSchema((input: unknown) => {
|
|
46
|
+
if (typeof input !== "string") {
|
|
47
|
+
throw new Error("Expected string");
|
|
48
|
+
}
|
|
49
|
+
return input;
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
number: (): IValidationSchema<number> => {
|
|
54
|
+
return new MockValidationSchema((input: unknown) => {
|
|
55
|
+
if (typeof input !== "number") {
|
|
56
|
+
throw new Error("Expected number");
|
|
57
|
+
}
|
|
58
|
+
return input;
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
boolean: (): IValidationSchema<boolean> => {
|
|
63
|
+
return new MockValidationSchema((input: unknown) => {
|
|
64
|
+
if (typeof input !== "boolean") {
|
|
65
|
+
throw new Error("Expected boolean");
|
|
66
|
+
}
|
|
67
|
+
return input;
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
transform: <T, U>(schema: IValidationSchema<T>, transformer: (value: T) => U): IValidationSchema<U> => {
|
|
72
|
+
return new MockValidationSchema((input: unknown) => {
|
|
73
|
+
const validated = schema.parse(input);
|
|
74
|
+
return transformer(validated);
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
withDefaults: <T>(defaultValue: T): IValidationSchema<T> => {
|
|
79
|
+
return new MockValidationSchema((input: unknown) => {
|
|
80
|
+
return input === undefined ? defaultValue : (input as T);
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
describe("Generic Validation Interface", () => {
|
|
86
|
+
describe("Task Input Validation", () => {
|
|
87
|
+
it("should validate task input successfully with valid data", async () => {
|
|
88
|
+
const userSchema = mockSchema.object<{
|
|
89
|
+
name: string;
|
|
90
|
+
age: number;
|
|
91
|
+
}>({
|
|
92
|
+
name: "string",
|
|
93
|
+
age: "number",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const createUserTask = defineTask({
|
|
97
|
+
id: "task.createUser",
|
|
98
|
+
inputSchema: userSchema,
|
|
99
|
+
run: async (input) => {
|
|
100
|
+
return `Created user ${(input as any).name}`;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const app = defineResource({
|
|
105
|
+
id: "app",
|
|
106
|
+
register: [createUserTask],
|
|
107
|
+
dependencies: { createUserTask },
|
|
108
|
+
init: async (_, { createUserTask }) => {
|
|
109
|
+
const result = await createUserTask({
|
|
110
|
+
name: "John Doe",
|
|
111
|
+
age: 30,
|
|
112
|
+
});
|
|
113
|
+
expect(result).toBe("Created user John Doe");
|
|
114
|
+
return result;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await run(app);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should throw validation error for invalid task input", async () => {
|
|
122
|
+
const userSchema = new MockValidationSchema((input: unknown) => {
|
|
123
|
+
if (typeof input !== "object" || input === null) {
|
|
124
|
+
throw new Error("Expected object");
|
|
125
|
+
}
|
|
126
|
+
const obj = input as any;
|
|
127
|
+
if (typeof obj.name !== "string") {
|
|
128
|
+
throw new Error("name must be string");
|
|
129
|
+
}
|
|
130
|
+
if (typeof obj.age !== "number" || obj.age < 0) {
|
|
131
|
+
throw new Error("age must be positive number");
|
|
132
|
+
}
|
|
133
|
+
return obj;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const createUserTask = defineTask({
|
|
137
|
+
id: "task.createUser.invalid",
|
|
138
|
+
inputSchema: userSchema,
|
|
139
|
+
run: async (input) => {
|
|
140
|
+
return `Created user ${(input as any).name}`;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const app = defineResource({
|
|
145
|
+
id: "app",
|
|
146
|
+
register: [createUserTask],
|
|
147
|
+
dependencies: { createUserTask },
|
|
148
|
+
init: async (_, { createUserTask }) => {
|
|
149
|
+
await createUserTask({
|
|
150
|
+
name: "John Doe",
|
|
151
|
+
age: -5, // Invalid: negative age
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await expect(run(app)).rejects.toThrow(/Task input validation failed/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should transform input data when schema supports it", async () => {
|
|
160
|
+
const stringToNumberSchema = mockSchema.transform(
|
|
161
|
+
mockSchema.string(),
|
|
162
|
+
(val: string) => parseInt(val, 10)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const mathTask = defineTask({
|
|
166
|
+
id: "task.math",
|
|
167
|
+
inputSchema: stringToNumberSchema,
|
|
168
|
+
run: async (input: number) => {
|
|
169
|
+
expect(typeof input).toBe("number");
|
|
170
|
+
return input * 2;
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const app = defineResource({
|
|
175
|
+
id: "app",
|
|
176
|
+
register: [mathTask],
|
|
177
|
+
dependencies: { mathTask },
|
|
178
|
+
init: async (_, { mathTask }) => {
|
|
179
|
+
const result = await (mathTask as any)("42"); // String input should be transformed to number
|
|
180
|
+
expect(result).toBe(84);
|
|
181
|
+
return result;
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await run(app);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("Resource Config Validation", () => {
|
|
190
|
+
it("should validate resource config when .with() is called (fail fast)", async () => {
|
|
191
|
+
const configSchema = new MockValidationSchema((input: unknown) => {
|
|
192
|
+
if (typeof input !== "object" || input === null) {
|
|
193
|
+
throw new Error("Expected object");
|
|
194
|
+
}
|
|
195
|
+
const obj = input as any;
|
|
196
|
+
if (typeof obj.host !== "string") {
|
|
197
|
+
throw new Error("host must be string");
|
|
198
|
+
}
|
|
199
|
+
if (typeof obj.port !== "number" || obj.port < 1 || obj.port > 65535) {
|
|
200
|
+
throw new Error("port must be number between 1-65535");
|
|
201
|
+
}
|
|
202
|
+
return obj;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const databaseResource = defineResource({
|
|
206
|
+
id: "resource.database",
|
|
207
|
+
configSchema: configSchema,
|
|
208
|
+
init: async (config) => {
|
|
209
|
+
return {
|
|
210
|
+
connect: () => `Connected to ${(config as any).host}:${(config as any).port}`,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// This should throw immediately when .with() is called, not during init
|
|
216
|
+
expect(() => {
|
|
217
|
+
databaseResource.with({
|
|
218
|
+
host: "localhost",
|
|
219
|
+
port: 99999, // Invalid: port too high
|
|
220
|
+
});
|
|
221
|
+
}).toThrow(/Resource config validation failed/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should work with valid resource config", async () => {
|
|
225
|
+
const configSchema = mockSchema.object<{
|
|
226
|
+
host: string;
|
|
227
|
+
port: number;
|
|
228
|
+
}>({
|
|
229
|
+
host: "string",
|
|
230
|
+
port: "number",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const databaseResource = defineResource({
|
|
234
|
+
id: "resource.database.valid",
|
|
235
|
+
configSchema: configSchema,
|
|
236
|
+
init: async (config) => {
|
|
237
|
+
return {
|
|
238
|
+
connect: () => `Connected to ${(config as any).host}:${(config as any).port}`,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const app = defineResource({
|
|
244
|
+
id: "app",
|
|
245
|
+
register: [
|
|
246
|
+
databaseResource.with({
|
|
247
|
+
host: "localhost",
|
|
248
|
+
port: 5432,
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
dependencies: { database: databaseResource },
|
|
252
|
+
init: async (_, { database }) => {
|
|
253
|
+
const result = database.connect();
|
|
254
|
+
expect(result).toBe("Connected to localhost:5432");
|
|
255
|
+
return result;
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await run(app);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("Event Payload Validation", () => {
|
|
264
|
+
it("should validate event payload when emitted", async () => {
|
|
265
|
+
const payloadSchema = new MockValidationSchema((input: unknown) => {
|
|
266
|
+
if (typeof input !== "object" || input === null) {
|
|
267
|
+
throw new Error("Expected object");
|
|
268
|
+
}
|
|
269
|
+
const obj = input as any;
|
|
270
|
+
if (typeof obj.message !== "string") {
|
|
271
|
+
throw new Error("message must be string");
|
|
272
|
+
}
|
|
273
|
+
return obj;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const testEvent = defineEvent({
|
|
277
|
+
id: "event.test",
|
|
278
|
+
payloadSchema: payloadSchema,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let receivedMessage = "";
|
|
282
|
+
const listenerTask = defineTask({
|
|
283
|
+
id: "task.listener",
|
|
284
|
+
on: testEvent,
|
|
285
|
+
run: async (event) => {
|
|
286
|
+
receivedMessage = (event.data as any).message;
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const app = defineResource({
|
|
291
|
+
id: "app",
|
|
292
|
+
register: [testEvent, listenerTask],
|
|
293
|
+
dependencies: { testEvent },
|
|
294
|
+
init: async (_, { testEvent }) => {
|
|
295
|
+
// This should work with valid payload
|
|
296
|
+
await testEvent({ message: "Hello World" });
|
|
297
|
+
expect(receivedMessage).toBe("Hello World");
|
|
298
|
+
|
|
299
|
+
// This should throw with invalid payload
|
|
300
|
+
await expect(testEvent({ invalidField: 123 } as any)).rejects.toThrow(/Event payload validation failed/);
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await run(app);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("Middleware Config Validation", () => {
|
|
309
|
+
it("should validate middleware config when .with() is called (fail fast)", async () => {
|
|
310
|
+
const configSchema = new MockValidationSchema((input: unknown) => {
|
|
311
|
+
if (typeof input !== "object" || input === null) {
|
|
312
|
+
throw new Error("Expected object");
|
|
313
|
+
}
|
|
314
|
+
const obj = input as any;
|
|
315
|
+
if (typeof obj.timeout !== "number" || obj.timeout <= 0) {
|
|
316
|
+
throw new Error("timeout must be positive number");
|
|
317
|
+
}
|
|
318
|
+
return obj;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const timingMiddleware = defineMiddleware({
|
|
322
|
+
id: "middleware.timing",
|
|
323
|
+
configSchema: configSchema,
|
|
324
|
+
run: async ({ next }, _, config) => {
|
|
325
|
+
return next();
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// This should throw immediately when .with() is called
|
|
330
|
+
expect(() => {
|
|
331
|
+
timingMiddleware.with({
|
|
332
|
+
timeout: -5, // Invalid: negative timeout
|
|
333
|
+
});
|
|
334
|
+
}).toThrow(/Middleware config validation failed/);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should work with valid middleware config", async () => {
|
|
338
|
+
const configSchema = mockSchema.object<{
|
|
339
|
+
timeout: number;
|
|
340
|
+
}>({
|
|
341
|
+
timeout: "number",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const timingMiddleware = defineMiddleware({
|
|
345
|
+
id: "middleware.timing.valid",
|
|
346
|
+
configSchema: configSchema,
|
|
347
|
+
run: async ({ next }, _, config) => {
|
|
348
|
+
const start = Date.now();
|
|
349
|
+
const result = await next();
|
|
350
|
+
const duration = Date.now() - start;
|
|
351
|
+
expect(typeof (config as any).timeout).toBe("number");
|
|
352
|
+
return result;
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const testTask = defineTask({
|
|
357
|
+
id: "task.test",
|
|
358
|
+
middleware: [timingMiddleware.with({ timeout: 5000 })],
|
|
359
|
+
run: async () => {
|
|
360
|
+
return "success";
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const app = defineResource({
|
|
365
|
+
id: "app",
|
|
366
|
+
register: [timingMiddleware, testTask],
|
|
367
|
+
dependencies: { testTask },
|
|
368
|
+
init: async (_, { testTask }) => {
|
|
369
|
+
const result = await testTask();
|
|
370
|
+
expect(result).toBe("success");
|
|
371
|
+
return result;
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
await run(app);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("No Validation (Backward Compatibility)", () => {
|
|
380
|
+
it("should work without any validation schemas", async () => {
|
|
381
|
+
const task = defineTask({
|
|
382
|
+
id: "task.noValidation",
|
|
383
|
+
run: async (input: any) => {
|
|
384
|
+
return `Received: ${JSON.stringify(input)}`;
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const resource = defineResource({
|
|
389
|
+
id: "resource.noValidation",
|
|
390
|
+
init: async (config: any) => {
|
|
391
|
+
return { config };
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const event = defineEvent<any>({
|
|
396
|
+
id: "event.noValidation",
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const middleware = defineMiddleware({
|
|
400
|
+
id: "middleware.noValidation",
|
|
401
|
+
run: async ({ next }) => next(),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const app = defineResource({
|
|
405
|
+
id: "app",
|
|
406
|
+
register: [
|
|
407
|
+
task,
|
|
408
|
+
resource.with({ anything: "goes" }),
|
|
409
|
+
event,
|
|
410
|
+
middleware,
|
|
411
|
+
],
|
|
412
|
+
dependencies: { task, resource, event },
|
|
413
|
+
init: async (_, { task, resource, event }) => {
|
|
414
|
+
const taskResult = await task({ any: "data" });
|
|
415
|
+
expect(taskResult).toBe('Received: {"any":"data"}');
|
|
416
|
+
|
|
417
|
+
expect(resource.config.anything).toBe("goes");
|
|
418
|
+
|
|
419
|
+
await event({ any: "payload" }); // Should work without validation
|
|
420
|
+
|
|
421
|
+
return "success";
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
await run(app);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
package/src/define.ts
CHANGED
|
@@ -33,8 +33,10 @@ import {
|
|
|
33
33
|
symbolResourceWithConfig,
|
|
34
34
|
symbolResource,
|
|
35
35
|
symbolMiddleware,
|
|
36
|
+
ITaskMeta,
|
|
37
|
+
IResourceMeta,
|
|
36
38
|
} from "./defs";
|
|
37
|
-
import {
|
|
39
|
+
import { MiddlewareAlreadyGlobalError, ValidationError } from "./errors";
|
|
38
40
|
import { generateCallerIdFromFile, getCallerFile } from "./tools/getCallerFile";
|
|
39
41
|
|
|
40
42
|
// Helper function to get the caller file
|
|
@@ -43,10 +45,11 @@ export function defineTask<
|
|
|
43
45
|
Input = undefined,
|
|
44
46
|
Output extends Promise<any> = any,
|
|
45
47
|
Deps extends DependencyMapType = any,
|
|
46
|
-
TOn extends "*" | IEventDefinition | undefined = undefined
|
|
48
|
+
TOn extends "*" | IEventDefinition | undefined = undefined,
|
|
49
|
+
TMeta extends ITaskMeta = any
|
|
47
50
|
>(
|
|
48
|
-
taskConfig: ITaskDefinition<Input, Output, Deps, TOn>
|
|
49
|
-
): ITask<Input, Output, Deps, TOn> {
|
|
51
|
+
taskConfig: ITaskDefinition<Input, Output, Deps, TOn, TMeta>
|
|
52
|
+
): ITask<Input, Output, Deps, TOn, TMeta> {
|
|
50
53
|
/**
|
|
51
54
|
* Creates a task definition.
|
|
52
55
|
* - Generates an anonymous id based on file path when `id` is omitted
|
|
@@ -65,6 +68,7 @@ export function defineTask<
|
|
|
65
68
|
run: taskConfig.run,
|
|
66
69
|
on: taskConfig.on,
|
|
67
70
|
listenerOrder: taskConfig.listenerOrder,
|
|
71
|
+
inputSchema: taskConfig.inputSchema,
|
|
68
72
|
events: {
|
|
69
73
|
beforeRun: {
|
|
70
74
|
...defineEvent({
|
|
@@ -91,19 +95,28 @@ export function defineTask<
|
|
|
91
95
|
[symbolFilePath]: getCallerFile(),
|
|
92
96
|
},
|
|
93
97
|
},
|
|
94
|
-
meta: taskConfig.meta || {},
|
|
98
|
+
meta: taskConfig.meta || ({} as TMeta),
|
|
95
99
|
// autorun,
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
export function defineResource<
|
|
100
104
|
TConfig = void,
|
|
101
|
-
TValue = any
|
|
105
|
+
TValue extends Promise<any> = Promise<any>,
|
|
102
106
|
TDeps extends DependencyMapType = {},
|
|
103
|
-
TPrivate = any
|
|
107
|
+
TPrivate = any,
|
|
108
|
+
TMeta extends IResourceMeta = any
|
|
104
109
|
>(
|
|
105
|
-
constConfig: IResourceDefinition<
|
|
106
|
-
|
|
110
|
+
constConfig: IResourceDefinition<
|
|
111
|
+
TConfig,
|
|
112
|
+
TValue,
|
|
113
|
+
TDeps,
|
|
114
|
+
TPrivate,
|
|
115
|
+
any,
|
|
116
|
+
any,
|
|
117
|
+
TMeta
|
|
118
|
+
>
|
|
119
|
+
): IResource<TConfig, TValue, TDeps, TPrivate, TMeta> {
|
|
107
120
|
/**
|
|
108
121
|
* Creates a resource definition.
|
|
109
122
|
* - Generates anonymous id when omitted (resource or index flavor)
|
|
@@ -129,7 +142,17 @@ export function defineResource<
|
|
|
129
142
|
overrides: constConfig.overrides || [],
|
|
130
143
|
init: constConfig.init,
|
|
131
144
|
context: constConfig.context,
|
|
145
|
+
configSchema: constConfig.configSchema,
|
|
132
146
|
with: function (config: TConfig) {
|
|
147
|
+
// Validate config with schema if provided (fail fast)
|
|
148
|
+
if (this.configSchema) {
|
|
149
|
+
try {
|
|
150
|
+
config = this.configSchema.parse(config);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new ValidationError("Resource config", this.id, error instanceof Error ? error : new Error(String(error)));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
133
156
|
return {
|
|
134
157
|
[symbolResourceWithConfig]: true,
|
|
135
158
|
id: this.id,
|
|
@@ -164,7 +187,7 @@ export function defineResource<
|
|
|
164
187
|
[symbolFilePath]: filePath,
|
|
165
188
|
},
|
|
166
189
|
},
|
|
167
|
-
meta: constConfig.meta || {},
|
|
190
|
+
meta: (constConfig.meta || {}) as TMeta,
|
|
168
191
|
middleware: constConfig.middleware || [],
|
|
169
192
|
};
|
|
170
193
|
}
|
|
@@ -182,7 +205,7 @@ export function defineIndex<
|
|
|
182
205
|
? T[K]["resource"]
|
|
183
206
|
: T[K];
|
|
184
207
|
} & DependencyMapType
|
|
185
|
-
>(items: T): IResource<void, DependencyValuesType<D
|
|
208
|
+
>(items: T): IResource<void, Promise<DependencyValuesType<D>>, D> {
|
|
186
209
|
// Build dependency map from given items; unwrap `.with()` to the base resource
|
|
187
210
|
const dependencies = {} as D;
|
|
188
211
|
const register: RegisterableItems[] = [];
|
|
@@ -263,6 +286,15 @@ export function defineMiddleware<
|
|
|
263
286
|
return {
|
|
264
287
|
...object,
|
|
265
288
|
with: (config: TConfig) => {
|
|
289
|
+
// Validate config with schema if provided (fail fast)
|
|
290
|
+
if (object.configSchema) {
|
|
291
|
+
try {
|
|
292
|
+
config = object.configSchema.parse(config);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw new ValidationError("Middleware config", object.id, error instanceof Error ? error : new Error(String(error)));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
266
298
|
return {
|
|
267
299
|
...object,
|
|
268
300
|
[symbolMiddlewareConfigured]: true,
|
|
@@ -280,7 +312,7 @@ export function defineMiddleware<
|
|
|
280
312
|
[symbolMiddlewareEverywhereTasks]: tasks,
|
|
281
313
|
[symbolMiddlewareEverywhereResources]: resources,
|
|
282
314
|
everywhere() {
|
|
283
|
-
throw
|
|
315
|
+
throw new MiddlewareAlreadyGlobalError(object.id);
|
|
284
316
|
},
|
|
285
317
|
};
|
|
286
318
|
},
|
|
@@ -343,9 +375,9 @@ export function defineOverride(
|
|
|
343
375
|
* - `.with(config)` to create configured instances
|
|
344
376
|
* - `.extract(tags)` to extract this tag from a list of tags
|
|
345
377
|
*/
|
|
346
|
-
export function defineTag<TConfig = void>(
|
|
347
|
-
definition: ITagDefinition<TConfig>
|
|
348
|
-
): ITag<TConfig> {
|
|
378
|
+
export function defineTag<TConfig = void, TEnforceContract = void>(
|
|
379
|
+
definition: ITagDefinition<TConfig, TEnforceContract>
|
|
380
|
+
): ITag<TConfig, TEnforceContract> {
|
|
349
381
|
const id = definition.id;
|
|
350
382
|
|
|
351
383
|
return {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// HasContracts<Meta> → true if contracts present, else false
|
|
2
|
+
|
|
3
|
+
import { ITag, ITagWithConfig } from "./defs";
|
|
4
|
+
import { IMeta } from "./defs";
|
|
5
|
+
|
|
6
|
+
// Keep these param names aligned with your defs.ts: ITag<TConfig, TEnforceContract>
|
|
7
|
+
type NonVoid<T> = [T] extends [void] ? never : T;
|
|
8
|
+
|
|
9
|
+
type ExtractReturnFromTag<T> = T extends ITagWithConfig<any, infer R>
|
|
10
|
+
? NonVoid<R>
|
|
11
|
+
: T extends ITag<any, infer R>
|
|
12
|
+
? NonVoid<R>
|
|
13
|
+
: never;
|
|
14
|
+
|
|
15
|
+
type IsTuple<T extends readonly unknown[]> = number extends T["length"]
|
|
16
|
+
? false
|
|
17
|
+
: true;
|
|
18
|
+
|
|
19
|
+
type FilterContracts<
|
|
20
|
+
TTags extends readonly unknown[],
|
|
21
|
+
Acc extends readonly unknown[] = []
|
|
22
|
+
> = TTags extends readonly [infer H, ...infer R]
|
|
23
|
+
? ExtractReturnFromTag<H> extends never
|
|
24
|
+
? FilterContracts<R, Acc>
|
|
25
|
+
: FilterContracts<R, [...Acc, ExtractReturnFromTag<H>]>
|
|
26
|
+
: Acc;
|
|
27
|
+
|
|
28
|
+
export type ExtractContractsFromTags<TTags extends readonly unknown[]> =
|
|
29
|
+
IsTuple<TTags> extends true
|
|
30
|
+
? FilterContracts<TTags>
|
|
31
|
+
: Array<ExtractReturnFromTag<TTags[number]>>;
|
|
32
|
+
|
|
33
|
+
export type ExtractTagsWithNonVoidReturnTypeFromMeta<TMeta extends IMeta> =
|
|
34
|
+
TMeta extends { tags?: infer TTags }
|
|
35
|
+
? TTags extends readonly unknown[]
|
|
36
|
+
? ExtractContractsFromTags<TTags>
|
|
37
|
+
: []
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
type IsNeverTuple<T extends readonly unknown[]> = T extends [] ? true : false;
|
|
41
|
+
|
|
42
|
+
export type HasContracts<T extends IMeta> =
|
|
43
|
+
ExtractTagsWithNonVoidReturnTypeFromMeta<T> extends never[] ? false : true; // HasContracts and enforcement
|
|
44
|
+
|
|
45
|
+
// Ensure a response type satisfies ALL contracts (intersection)
|
|
46
|
+
type UnionToIntersection<U> = (
|
|
47
|
+
U extends any ? (arg: U) => void : never
|
|
48
|
+
) extends (arg: infer I) => void
|
|
49
|
+
? I
|
|
50
|
+
: never;
|
|
51
|
+
|
|
52
|
+
type ContractsUnion<TMeta extends IMeta> =
|
|
53
|
+
ExtractTagsWithNonVoidReturnTypeFromMeta<TMeta> extends readonly (infer U)[]
|
|
54
|
+
? U
|
|
55
|
+
: never;
|
|
56
|
+
|
|
57
|
+
type ContractsIntersection<TMeta extends IMeta> = UnionToIntersection<
|
|
58
|
+
ContractsUnion<TMeta>
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pretty-print helper to expand intersections for better IDE display.
|
|
63
|
+
*/
|
|
64
|
+
type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Verbose compile-time error surfaced when a value does not satisfy
|
|
68
|
+
* the intersection of all tag-enforced contracts.
|
|
69
|
+
*
|
|
70
|
+
* Intersected with `never` in call sites when desired to ensure assignment
|
|
71
|
+
* fails while still surfacing a readable shape in tooltips.
|
|
72
|
+
*/
|
|
73
|
+
export type ContractViolationError<TMeta extends IMeta, TActual> = {
|
|
74
|
+
message: "Value does not satisfy all tag contracts";
|
|
75
|
+
expected: Simplify<ContractsIntersection<TMeta>>;
|
|
76
|
+
received: TActual;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type EnsureResponseSatisfiesContracts<TMeta extends IMeta, TResponse> = [
|
|
80
|
+
ContractsUnion<TMeta>
|
|
81
|
+
] extends [never]
|
|
82
|
+
? TResponse // no contracts, allow as-is
|
|
83
|
+
: TResponse extends Promise<infer U>
|
|
84
|
+
? Promise<
|
|
85
|
+
U extends ContractsIntersection<TMeta>
|
|
86
|
+
? U
|
|
87
|
+
: ContractViolationError<TMeta, U>
|
|
88
|
+
>
|
|
89
|
+
: TResponse extends ContractsIntersection<TMeta>
|
|
90
|
+
? TResponse
|
|
91
|
+
: ContractViolationError<TMeta, TResponse>;
|