@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation for Bueno Framework
|
|
3
|
+
* Supports Standard Schema validators (Zod, Valibot, ArkType)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { StandardSchema, StandardIssue } from "../types";
|
|
7
|
+
import type { BuenoConfig, DeepPartial } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validation result
|
|
11
|
+
*/
|
|
12
|
+
export interface ConfigValidationResult {
|
|
13
|
+
/** Whether validation passed */
|
|
14
|
+
valid: boolean;
|
|
15
|
+
/** Validation errors if any */
|
|
16
|
+
errors: ConfigValidationError[];
|
|
17
|
+
/** Warnings (non-critical issues) */
|
|
18
|
+
warnings: ConfigValidationWarning[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validation error
|
|
23
|
+
*/
|
|
24
|
+
export interface ConfigValidationError {
|
|
25
|
+
/** Error message */
|
|
26
|
+
message: string;
|
|
27
|
+
/** Path to the invalid field */
|
|
28
|
+
path?: string;
|
|
29
|
+
/** Expected type or value */
|
|
30
|
+
expected?: string;
|
|
31
|
+
/** Actual value received */
|
|
32
|
+
received?: unknown;
|
|
33
|
+
/** Original issue from schema validator */
|
|
34
|
+
issue?: StandardIssue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validation warning
|
|
39
|
+
*/
|
|
40
|
+
export interface ConfigValidationWarning {
|
|
41
|
+
/** Warning message */
|
|
42
|
+
message: string;
|
|
43
|
+
/** Path to the field */
|
|
44
|
+
path?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a value is a Standard Schema
|
|
49
|
+
*/
|
|
50
|
+
export function isStandardSchema(value: unknown): value is StandardSchema {
|
|
51
|
+
return (
|
|
52
|
+
typeof value === "object" &&
|
|
53
|
+
value !== null &&
|
|
54
|
+
"~standard" in value &&
|
|
55
|
+
typeof (value as StandardSchema)["~standard"] === "object" &&
|
|
56
|
+
typeof (value as StandardSchema)["~standard"].validate === "function"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate config against a Standard Schema
|
|
62
|
+
*/
|
|
63
|
+
export async function validateWithSchema<T>(
|
|
64
|
+
config: unknown,
|
|
65
|
+
schema: StandardSchema<T>,
|
|
66
|
+
): Promise<ConfigValidationResult> {
|
|
67
|
+
const result = await schema["~standard"].validate(config);
|
|
68
|
+
|
|
69
|
+
if (result.issues === undefined) {
|
|
70
|
+
return {
|
|
71
|
+
valid: true,
|
|
72
|
+
errors: [],
|
|
73
|
+
warnings: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const errors: ConfigValidationError[] = result.issues.map((issue) => ({
|
|
78
|
+
message: issue.message,
|
|
79
|
+
path: formatPath(issue.path),
|
|
80
|
+
issue,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
errors,
|
|
86
|
+
warnings: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format a path from Standard Schema issues
|
|
92
|
+
*/
|
|
93
|
+
function formatPath(
|
|
94
|
+
path?: ReadonlyArray<PropertyKey | { key: PropertyKey }>,
|
|
95
|
+
): string | undefined {
|
|
96
|
+
if (!path || path.length === 0) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return path
|
|
101
|
+
.map((segment) => {
|
|
102
|
+
if (typeof segment === "object" && "key" in segment) {
|
|
103
|
+
return String(segment.key);
|
|
104
|
+
}
|
|
105
|
+
return String(segment);
|
|
106
|
+
})
|
|
107
|
+
.join(".");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Default validation rules for BuenoConfig
|
|
112
|
+
*/
|
|
113
|
+
const DEFAULT_RULES: ValidationRule[] = [
|
|
114
|
+
{
|
|
115
|
+
path: "server.port",
|
|
116
|
+
validate: (value) => {
|
|
117
|
+
if (value === undefined) return { valid: true };
|
|
118
|
+
if (typeof value !== "number") {
|
|
119
|
+
return { valid: false, message: "Port must be a number" };
|
|
120
|
+
}
|
|
121
|
+
if (value < 0 || value > 65535) {
|
|
122
|
+
return { valid: false, message: "Port must be between 0 and 65535" };
|
|
123
|
+
}
|
|
124
|
+
return { valid: true };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
path: "database.poolSize",
|
|
129
|
+
validate: (value) => {
|
|
130
|
+
if (value === undefined) return { valid: true };
|
|
131
|
+
if (typeof value !== "number") {
|
|
132
|
+
return { valid: false, message: "Pool size must be a number" };
|
|
133
|
+
}
|
|
134
|
+
if (value < 1) {
|
|
135
|
+
return { valid: false, message: "Pool size must be at least 1" };
|
|
136
|
+
}
|
|
137
|
+
return { valid: true };
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
path: "database.slowQueryThreshold",
|
|
142
|
+
validate: (value) => {
|
|
143
|
+
if (value === undefined) return { valid: true };
|
|
144
|
+
if (typeof value !== "number") {
|
|
145
|
+
return { valid: false, message: "Slow query threshold must be a number" };
|
|
146
|
+
}
|
|
147
|
+
if (value < 0) {
|
|
148
|
+
return { valid: false, message: "Slow query threshold must be non-negative" };
|
|
149
|
+
}
|
|
150
|
+
return { valid: true };
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: "cache.ttl",
|
|
155
|
+
validate: (value) => {
|
|
156
|
+
if (value === undefined) return { valid: true };
|
|
157
|
+
if (typeof value !== "number") {
|
|
158
|
+
return { valid: false, message: "TTL must be a number" };
|
|
159
|
+
}
|
|
160
|
+
if (value < 0) {
|
|
161
|
+
return { valid: false, message: "TTL must be non-negative" };
|
|
162
|
+
}
|
|
163
|
+
return { valid: true };
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
path: "cache.driver",
|
|
168
|
+
validate: (value) => {
|
|
169
|
+
if (value === undefined) return { valid: true };
|
|
170
|
+
if (value !== "redis" && value !== "memory") {
|
|
171
|
+
return {
|
|
172
|
+
valid: false,
|
|
173
|
+
message: 'Cache driver must be "redis" or "memory"',
|
|
174
|
+
expected: '"redis" | "memory"',
|
|
175
|
+
received: value,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { valid: true };
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
path: "logger.level",
|
|
183
|
+
validate: (value) => {
|
|
184
|
+
if (value === undefined) return { valid: true };
|
|
185
|
+
const validLevels = ["debug", "info", "warn", "error", "fatal"];
|
|
186
|
+
if (!validLevels.includes(value as string)) {
|
|
187
|
+
return {
|
|
188
|
+
valid: false,
|
|
189
|
+
message: `Logger level must be one of: ${validLevels.join(", ")}`,
|
|
190
|
+
expected: validLevels.join(" | "),
|
|
191
|
+
received: value,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { valid: true };
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
path: "telemetry.sampleRate",
|
|
199
|
+
validate: (value) => {
|
|
200
|
+
if (value === undefined) return { valid: true };
|
|
201
|
+
if (typeof value !== "number") {
|
|
202
|
+
return { valid: false, message: "Sample rate must be a number" };
|
|
203
|
+
}
|
|
204
|
+
if (value < 0 || value > 1) {
|
|
205
|
+
return { valid: false, message: "Sample rate must be between 0 and 1" };
|
|
206
|
+
}
|
|
207
|
+
return { valid: true };
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
path: "metrics.collectInterval",
|
|
212
|
+
validate: (value) => {
|
|
213
|
+
if (value === undefined) return { valid: true };
|
|
214
|
+
if (typeof value !== "number") {
|
|
215
|
+
return { valid: false, message: "Collect interval must be a number" };
|
|
216
|
+
}
|
|
217
|
+
if (value < 0) {
|
|
218
|
+
return { valid: false, message: "Collect interval must be non-negative" };
|
|
219
|
+
}
|
|
220
|
+
return { valid: true };
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Validation rule
|
|
227
|
+
*/
|
|
228
|
+
interface ValidationRule {
|
|
229
|
+
/** Path to the field to validate */
|
|
230
|
+
path: string;
|
|
231
|
+
/** Validation function */
|
|
232
|
+
validate: (
|
|
233
|
+
value: unknown,
|
|
234
|
+
config: DeepPartial<BuenoConfig>,
|
|
235
|
+
) => ValidationResult;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validation result from a rule
|
|
240
|
+
*/
|
|
241
|
+
interface ValidationResult {
|
|
242
|
+
valid: boolean;
|
|
243
|
+
message?: string;
|
|
244
|
+
expected?: string;
|
|
245
|
+
received?: unknown;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get a value from an object using dot notation path
|
|
250
|
+
*/
|
|
251
|
+
function getValueByPath(obj: unknown, path: string): unknown {
|
|
252
|
+
const parts = path.split(".");
|
|
253
|
+
let current: unknown = obj;
|
|
254
|
+
|
|
255
|
+
for (const part of parts) {
|
|
256
|
+
if (current === null || current === undefined) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (typeof current !== "object") {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
current = (current as Record<string, unknown>)[part];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return current;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Validate config using default rules
|
|
270
|
+
*/
|
|
271
|
+
export function validateConfigDefaults(
|
|
272
|
+
config: DeepPartial<BuenoConfig>,
|
|
273
|
+
): ConfigValidationResult {
|
|
274
|
+
const errors: ConfigValidationError[] = [];
|
|
275
|
+
const warnings: ConfigValidationWarning[] = [];
|
|
276
|
+
|
|
277
|
+
for (const rule of DEFAULT_RULES) {
|
|
278
|
+
const value = getValueByPath(config, rule.path);
|
|
279
|
+
const result = rule.validate(value, config);
|
|
280
|
+
|
|
281
|
+
if (!result.valid) {
|
|
282
|
+
errors.push({
|
|
283
|
+
message: result.message || "Validation failed",
|
|
284
|
+
path: rule.path,
|
|
285
|
+
expected: result.expected,
|
|
286
|
+
received: result.received,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Add warnings for potentially missing critical config
|
|
292
|
+
if (!config.database?.url && !process.env.DATABASE_URL) {
|
|
293
|
+
warnings.push({
|
|
294
|
+
message: "No database URL configured",
|
|
295
|
+
path: "database.url",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (config.cache?.driver === "redis" && !config.cache.url && !process.env.REDIS_URL) {
|
|
300
|
+
warnings.push({
|
|
301
|
+
message: "Redis cache driver selected but no Redis URL configured",
|
|
302
|
+
path: "cache.url",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
valid: errors.length === 0,
|
|
308
|
+
errors,
|
|
309
|
+
warnings,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validate configuration
|
|
315
|
+
* Supports both Standard Schema and default validation rules
|
|
316
|
+
*/
|
|
317
|
+
export async function validateConfig<T extends BuenoConfig = BuenoConfig>(
|
|
318
|
+
config: unknown,
|
|
319
|
+
schema?: StandardSchema<T>,
|
|
320
|
+
): Promise<ConfigValidationResult> {
|
|
321
|
+
// If a schema is provided, use it
|
|
322
|
+
if (schema && isStandardSchema(schema)) {
|
|
323
|
+
return validateWithSchema(config, schema);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Otherwise, use default validation
|
|
327
|
+
return validateConfigDefaults(config as DeepPartial<BuenoConfig>);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Validate configuration synchronously
|
|
332
|
+
* Note: Standard Schema validation may be async, so this only works with default rules
|
|
333
|
+
*/
|
|
334
|
+
export function validateConfigSync(
|
|
335
|
+
config: DeepPartial<BuenoConfig>,
|
|
336
|
+
): ConfigValidationResult {
|
|
337
|
+
return validateConfigDefaults(config);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a validation error with helpful message
|
|
342
|
+
*/
|
|
343
|
+
export function createConfigError(
|
|
344
|
+
result: ConfigValidationResult,
|
|
345
|
+
): ConfigValidationError {
|
|
346
|
+
if (result.errors.length === 0) {
|
|
347
|
+
return {
|
|
348
|
+
message: "Unknown validation error",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Return the first error with context
|
|
353
|
+
const firstError = result.errors[0];
|
|
354
|
+
return {
|
|
355
|
+
message: firstError.message,
|
|
356
|
+
path: firstError.path,
|
|
357
|
+
expected: firstError.expected,
|
|
358
|
+
received: firstError.received,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Format validation errors for display
|
|
364
|
+
*/
|
|
365
|
+
export function formatValidationErrors(
|
|
366
|
+
errors: ConfigValidationError[],
|
|
367
|
+
): string {
|
|
368
|
+
if (errors.length === 0) {
|
|
369
|
+
return "No errors";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const lines = errors.map((error, index) => {
|
|
373
|
+
let line = `${index + 1}. ${error.message}`;
|
|
374
|
+
if (error.path) {
|
|
375
|
+
line += ` (at ${error.path})`;
|
|
376
|
+
}
|
|
377
|
+
if (error.expected) {
|
|
378
|
+
line += `\n Expected: ${error.expected}`;
|
|
379
|
+
}
|
|
380
|
+
if (error.received !== undefined) {
|
|
381
|
+
line += `\n Received: ${JSON.stringify(error.received)}`;
|
|
382
|
+
}
|
|
383
|
+
return line;
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return `Configuration validation failed:\n${lines.join("\n")}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Assert that configuration is valid
|
|
391
|
+
* Throws an error if validation fails
|
|
392
|
+
*/
|
|
393
|
+
export async function assertValidConfig<T extends BuenoConfig = BuenoConfig>(
|
|
394
|
+
config: unknown,
|
|
395
|
+
schema?: StandardSchema<T>,
|
|
396
|
+
): Promise<void> {
|
|
397
|
+
const result = await validateConfig(config, schema);
|
|
398
|
+
|
|
399
|
+
if (!result.valid) {
|
|
400
|
+
const errorMessage = formatValidationErrors(result.errors);
|
|
401
|
+
throw new Error(errorMessage);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Log warnings
|
|
405
|
+
if (result.warnings.length > 0) {
|
|
406
|
+
for (const warning of result.warnings) {
|
|
407
|
+
console.warn(`Config warning: ${warning.message}${warning.path ? ` (at ${warning.path})` : ""}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Add custom validation rules
|
|
414
|
+
*/
|
|
415
|
+
export function createCustomValidator(
|
|
416
|
+
rules: ValidationRule[],
|
|
417
|
+
): (config: DeepPartial<BuenoConfig>) => ConfigValidationResult {
|
|
418
|
+
return (config) => {
|
|
419
|
+
const errors: ConfigValidationError[] = [];
|
|
420
|
+
|
|
421
|
+
for (const rule of rules) {
|
|
422
|
+
const value = getValueByPath(config, rule.path);
|
|
423
|
+
const result = rule.validate(value, config);
|
|
424
|
+
|
|
425
|
+
if (!result.valid) {
|
|
426
|
+
errors.push({
|
|
427
|
+
message: result.message || "Validation failed",
|
|
428
|
+
path: rule.path,
|
|
429
|
+
expected: result.expected,
|
|
430
|
+
received: result.received,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
valid: errors.length === 0,
|
|
437
|
+
errors,
|
|
438
|
+
warnings: [],
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forward Reference for Circular Dependencies
|
|
3
|
+
*
|
|
4
|
+
* Provides a way to resolve circular dependencies by deferring the resolution
|
|
5
|
+
* of a dependency until it's actually needed. This allows two or more services
|
|
6
|
+
* to depend on each other without causing infinite loops during instantiation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Forward reference container for lazy resolution of circular dependencies.
|
|
11
|
+
*
|
|
12
|
+
* @template T - The type of the referenced value
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Creating a forward reference
|
|
17
|
+
* const ref = forwardRef(() => ServiceB);
|
|
18
|
+
*
|
|
19
|
+
* // Using with @Inject decorator
|
|
20
|
+
* @Injectable()
|
|
21
|
+
* class ServiceA {
|
|
22
|
+
* constructor(
|
|
23
|
+
* @Inject(forwardRef(() => ServiceB))
|
|
24
|
+
* private serviceB: ServiceB
|
|
25
|
+
* ) {}
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export interface ForwardRef<T> {
|
|
30
|
+
/**
|
|
31
|
+
* The unique symbol identifying this as a ForwardRef
|
|
32
|
+
*/
|
|
33
|
+
readonly __forwardRef: unique symbol;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Factory function that returns the actual value when called.
|
|
37
|
+
* This is invoked lazily when the dependency is first accessed.
|
|
38
|
+
*/
|
|
39
|
+
forwardRef: () => T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Symbol used to identify ForwardRef objects
|
|
44
|
+
*/
|
|
45
|
+
const FORWARD_REF_SYMBOL = Symbol.for('buno.forwardRef');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a forward reference for circular dependency resolution.
|
|
49
|
+
*
|
|
50
|
+
* The provided factory function is called lazily when the dependency
|
|
51
|
+
* is actually resolved, allowing the referenced class to be defined
|
|
52
|
+
* later in the module loading process.
|
|
53
|
+
*
|
|
54
|
+
* @template T - The type of the referenced value
|
|
55
|
+
* @param fn - Factory function that returns the actual token or value
|
|
56
|
+
* @returns A ForwardRef object that can be used with @Inject()
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // service-a.ts
|
|
61
|
+
* @Injectable()
|
|
62
|
+
* export class ServiceA {
|
|
63
|
+
* constructor(
|
|
64
|
+
* @Inject(forwardRef(() => ServiceB))
|
|
65
|
+
* private serviceB: ServiceB
|
|
66
|
+
* ) {}
|
|
67
|
+
*
|
|
68
|
+
* doSomething() {
|
|
69
|
+
* return this.serviceB.help();
|
|
70
|
+
* }
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* // service-b.ts
|
|
74
|
+
* @Injectable()
|
|
75
|
+
* export class ServiceB {
|
|
76
|
+
* constructor(
|
|
77
|
+
* @Inject(forwardRef(() => ServiceA))
|
|
78
|
+
* private serviceA: ServiceA
|
|
79
|
+
* ) {}
|
|
80
|
+
*
|
|
81
|
+
* help() {
|
|
82
|
+
* return 'helping';
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function forwardRef<T>(fn: () => T): ForwardRef<T> {
|
|
88
|
+
return {
|
|
89
|
+
__forwardRef: FORWARD_REF_SYMBOL,
|
|
90
|
+
forwardRef: fn,
|
|
91
|
+
} as unknown as ForwardRef<T>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Type guard to check if a value is a ForwardRef.
|
|
96
|
+
*
|
|
97
|
+
* @param value - The value to check
|
|
98
|
+
* @returns True if the value is a ForwardRef, false otherwise
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const ref = forwardRef(() => MyService);
|
|
103
|
+
* if (isForwardRef(ref)) {
|
|
104
|
+
* const actualToken = resolveForwardRef(ref);
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function isForwardRef(value: unknown): value is ForwardRef<unknown> {
|
|
109
|
+
return (
|
|
110
|
+
typeof value === 'object' &&
|
|
111
|
+
value !== null &&
|
|
112
|
+
'__forwardRef' in value &&
|
|
113
|
+
'forwardRef' in value &&
|
|
114
|
+
typeof (value as ForwardRef<unknown>).forwardRef === 'function'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a forward reference to its actual value.
|
|
120
|
+
*
|
|
121
|
+
* If the provided value is a ForwardRef, this function calls its
|
|
122
|
+
* factory function to get the actual value. If it's not a ForwardRef,
|
|
123
|
+
* the value is returned as-is.
|
|
124
|
+
*
|
|
125
|
+
* @template T - The expected type of the resolved value
|
|
126
|
+
* @param ref - Either a ForwardRef or a direct value
|
|
127
|
+
* @returns The resolved value
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const token = Token<ServiceB>('ServiceB');
|
|
132
|
+
* const ref = forwardRef(() => token);
|
|
133
|
+
*
|
|
134
|
+
* // Resolves to the token
|
|
135
|
+
* const actualToken = resolveForwardRef(ref);
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function resolveForwardRef<T>(ref: ForwardRef<T> | T): T {
|
|
139
|
+
if (isForwardRef(ref)) {
|
|
140
|
+
return ref.forwardRef();
|
|
141
|
+
}
|
|
142
|
+
return ref;
|
|
143
|
+
}
|