@buenojs/bueno 0.8.3 → 0.8.5
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 +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Schema System
|
|
3
|
+
* Lightweight validation schema builder for common use cases.
|
|
4
|
+
* No external dependencies required.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StandardIssue, StandardResult } from "../types";
|
|
8
|
+
|
|
9
|
+
// ============= Schema Definition Types =============
|
|
10
|
+
|
|
11
|
+
export type FieldValidator = (value: unknown) => {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
value?: unknown; // Coerced/transformed value
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface SchemaDefinition {
|
|
18
|
+
[key: string]: FieldValidator | SchemaDefinition | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============= Built-in Field Validators =============
|
|
22
|
+
|
|
23
|
+
export const Fields = {
|
|
24
|
+
/**
|
|
25
|
+
* String field validators
|
|
26
|
+
*/
|
|
27
|
+
string: (
|
|
28
|
+
options: {
|
|
29
|
+
min?: number;
|
|
30
|
+
max?: number;
|
|
31
|
+
pattern?: RegExp;
|
|
32
|
+
email?: boolean;
|
|
33
|
+
url?: boolean;
|
|
34
|
+
uuid?: boolean;
|
|
35
|
+
optional?: boolean;
|
|
36
|
+
} = {},
|
|
37
|
+
) => {
|
|
38
|
+
return (value: unknown) => {
|
|
39
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
40
|
+
return { valid: true, value: undefined };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof value !== "string") {
|
|
44
|
+
return { valid: false, error: "Must be a string" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options.min !== undefined && value.length < options.min) {
|
|
48
|
+
return {
|
|
49
|
+
valid: false,
|
|
50
|
+
error: `Must be at least ${options.min} characters`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (options.max !== undefined && value.length > options.max) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
error: `Must be at most ${options.max} characters`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (options.pattern && !options.pattern.test(value)) {
|
|
62
|
+
return { valid: false, error: "Does not match required pattern" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (options.email) {
|
|
66
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
67
|
+
if (!emailRegex.test(value)) {
|
|
68
|
+
return { valid: false, error: "Must be a valid email address" };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.url) {
|
|
73
|
+
try {
|
|
74
|
+
new URL(value);
|
|
75
|
+
} catch {
|
|
76
|
+
return { valid: false, error: "Must be a valid URL" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (options.uuid) {
|
|
81
|
+
const uuidRegex =
|
|
82
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
83
|
+
if (!uuidRegex.test(value)) {
|
|
84
|
+
return { valid: false, error: "Must be a valid UUID" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { valid: true, value };
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Number field validators (coerces strings to numbers)
|
|
94
|
+
*/
|
|
95
|
+
number: (
|
|
96
|
+
options: {
|
|
97
|
+
min?: number;
|
|
98
|
+
max?: number;
|
|
99
|
+
integer?: boolean;
|
|
100
|
+
positive?: boolean;
|
|
101
|
+
optional?: boolean;
|
|
102
|
+
default?: number;
|
|
103
|
+
coerce?: boolean; // Default: true, coerce strings to numbers
|
|
104
|
+
} = {},
|
|
105
|
+
) => {
|
|
106
|
+
const shouldCoerce = options.coerce !== false; // Default true
|
|
107
|
+
|
|
108
|
+
return (value: unknown, validateOnly = false) => {
|
|
109
|
+
// Apply default if value is missing
|
|
110
|
+
if (
|
|
111
|
+
(value === undefined || value === null) &&
|
|
112
|
+
options.default !== undefined
|
|
113
|
+
) {
|
|
114
|
+
return { valid: true, value: options.default };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
118
|
+
return { valid: true, value: undefined };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let num = value;
|
|
122
|
+
|
|
123
|
+
// Coerce string to number
|
|
124
|
+
if (shouldCoerce && typeof value === "string") {
|
|
125
|
+
num = Number(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof num !== "number" || Number.isNaN(num)) {
|
|
129
|
+
return { valid: false, error: "Must be a number" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.integer && !Number.isInteger(num)) {
|
|
133
|
+
return { valid: false, error: "Must be an integer" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.positive && num <= 0) {
|
|
137
|
+
return { valid: false, error: "Must be a positive number" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.min !== undefined && num < options.min) {
|
|
141
|
+
return { valid: false, error: `Must be at least ${options.min}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.max !== undefined && num > options.max) {
|
|
145
|
+
return { valid: false, error: `Must be at most ${options.max}` };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { valid: true, value: num };
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Boolean field validator
|
|
154
|
+
*/
|
|
155
|
+
boolean: (options: { optional?: boolean } = {}) => {
|
|
156
|
+
return (value: unknown) => {
|
|
157
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
158
|
+
return { valid: true, value: undefined };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof value !== "boolean") {
|
|
162
|
+
return { valid: false, error: "Must be a boolean" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { valid: true, value };
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Array field validators
|
|
171
|
+
*/
|
|
172
|
+
array: (
|
|
173
|
+
options: {
|
|
174
|
+
min?: number;
|
|
175
|
+
max?: number;
|
|
176
|
+
itemValidator?: FieldValidator;
|
|
177
|
+
optional?: boolean;
|
|
178
|
+
} = {},
|
|
179
|
+
) => {
|
|
180
|
+
return (value: unknown) => {
|
|
181
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
182
|
+
return { valid: true, value: undefined };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!Array.isArray(value)) {
|
|
186
|
+
return { valid: false, error: "Must be an array" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (options.min !== undefined && value.length < options.min) {
|
|
190
|
+
return {
|
|
191
|
+
valid: false,
|
|
192
|
+
error: `Must have at least ${options.min} items`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (options.max !== undefined && value.length > options.max) {
|
|
197
|
+
return {
|
|
198
|
+
valid: false,
|
|
199
|
+
error: `Must have at most ${options.max} items`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (options.itemValidator) {
|
|
204
|
+
for (let i = 0; i < value.length; i++) {
|
|
205
|
+
const result = options.itemValidator(value[i]);
|
|
206
|
+
if (!result.valid) {
|
|
207
|
+
return {
|
|
208
|
+
valid: false,
|
|
209
|
+
error: `Item ${i}: ${result.error}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { valid: true, value };
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Enum field validator
|
|
221
|
+
*/
|
|
222
|
+
enum: (
|
|
223
|
+
values: readonly (string | number)[],
|
|
224
|
+
options: { optional?: boolean } = {},
|
|
225
|
+
) => {
|
|
226
|
+
return (value: unknown) => {
|
|
227
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
228
|
+
return { valid: true, value: undefined };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!values.includes(value as string | number)) {
|
|
232
|
+
return {
|
|
233
|
+
valid: false,
|
|
234
|
+
error: `Must be one of: ${values.join(", ")}`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { valid: true, value };
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Custom validator function
|
|
244
|
+
*/
|
|
245
|
+
custom: (
|
|
246
|
+
validate: (value: unknown) => boolean,
|
|
247
|
+
message = "Validation failed",
|
|
248
|
+
) => {
|
|
249
|
+
return (value: unknown) => {
|
|
250
|
+
if (validate(value)) {
|
|
251
|
+
return { valid: true, value };
|
|
252
|
+
}
|
|
253
|
+
return { valid: false, error: message };
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Environment variable validators
|
|
259
|
+
*/
|
|
260
|
+
env: {
|
|
261
|
+
/**
|
|
262
|
+
* Required environment variable validator
|
|
263
|
+
*/
|
|
264
|
+
required: (options: { optional?: boolean } = {}) => {
|
|
265
|
+
return (value: unknown) => {
|
|
266
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
267
|
+
return { valid: true, value: undefined };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (value === undefined || value === null || value === "") {
|
|
271
|
+
return {
|
|
272
|
+
valid: false,
|
|
273
|
+
error: "This environment variable is required",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { valid: true, value };
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Port number validator (1-65535)
|
|
283
|
+
*/
|
|
284
|
+
port: (options: { optional?: boolean } = {}) => {
|
|
285
|
+
return (value: unknown) => {
|
|
286
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
287
|
+
return { valid: true, value: undefined };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const num = Number(value);
|
|
291
|
+
if (isNaN(num) || num < 1 || num > 65535) {
|
|
292
|
+
return {
|
|
293
|
+
valid: false,
|
|
294
|
+
error: "Must be a valid port number (1-65535)",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { valid: true, value: num };
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* URL validator
|
|
304
|
+
*/
|
|
305
|
+
url: (options: { optional?: boolean } = {}) => {
|
|
306
|
+
return (value: unknown) => {
|
|
307
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
308
|
+
return { valid: true, value: undefined };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (typeof value !== "string") {
|
|
312
|
+
return { valid: false, error: "Must be a string" };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
new URL(value);
|
|
317
|
+
return { valid: true, value };
|
|
318
|
+
} catch {
|
|
319
|
+
return { valid: false, error: "Must be a valid URL" };
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Database URL validator
|
|
326
|
+
*/
|
|
327
|
+
databaseUrl: (options: { optional?: boolean } = {}) => {
|
|
328
|
+
return (value: unknown) => {
|
|
329
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
330
|
+
return { valid: true, value: undefined };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (typeof value !== "string") {
|
|
334
|
+
return { valid: false, error: "Must be a string" };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const url = new URL(value);
|
|
339
|
+
const validSchemes = ["postgresql", "mysql", "sqlite", "mongodb"];
|
|
340
|
+
if (!validSchemes.includes(url.protocol.replace(/:$/, ""))) {
|
|
341
|
+
return {
|
|
342
|
+
valid: false,
|
|
343
|
+
error: `Must be a valid database URL (postgresql://, mysql://, sqlite://, mongodb://)`,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return { valid: true, value };
|
|
347
|
+
} catch {
|
|
348
|
+
return { valid: false, error: "Must be a valid database URL" };
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Boolean validator
|
|
355
|
+
*/
|
|
356
|
+
boolean: (options: { optional?: boolean } = {}) => {
|
|
357
|
+
return (value: unknown) => {
|
|
358
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
359
|
+
return { valid: true, value: undefined };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
typeof value !== "boolean" &&
|
|
364
|
+
value !== "true" &&
|
|
365
|
+
value !== "false"
|
|
366
|
+
) {
|
|
367
|
+
return { valid: false, error: "Must be a boolean (true/false)" };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { valid: true, value: value === "true" || value === true };
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Number validator
|
|
376
|
+
*/
|
|
377
|
+
number: (
|
|
378
|
+
options: {
|
|
379
|
+
min?: number;
|
|
380
|
+
max?: number;
|
|
381
|
+
optional?: boolean;
|
|
382
|
+
integer?: boolean;
|
|
383
|
+
} = {},
|
|
384
|
+
) => {
|
|
385
|
+
return (value: unknown) => {
|
|
386
|
+
if (options.optional && (value === undefined || value === null)) {
|
|
387
|
+
return { valid: true, value: undefined };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const num = Number(value);
|
|
391
|
+
if (isNaN(num)) {
|
|
392
|
+
return { valid: false, error: "Must be a number" };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (options.integer && !Number.isInteger(num)) {
|
|
396
|
+
return { valid: false, error: "Must be an integer" };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (options.min !== undefined && num < options.min) {
|
|
400
|
+
return { valid: false, error: `Must be at least ${options.min}` };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (options.max !== undefined && num > options.max) {
|
|
404
|
+
return { valid: false, error: `Must be at most ${options.max}` };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { valid: true, value: num };
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// ============= Schema Class =============
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Built-in schema validator
|
|
417
|
+
* Simple, zero-dependency schema validation
|
|
418
|
+
*/
|
|
419
|
+
export class Schema {
|
|
420
|
+
constructor(private definition: SchemaDefinition) {}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Create a new schema from field definitions
|
|
424
|
+
*/
|
|
425
|
+
static object<T extends SchemaDefinition>(definition: T): Schema {
|
|
426
|
+
return new Schema(definition);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Validate data synchronously
|
|
431
|
+
*/
|
|
432
|
+
validateSync(data: unknown): StandardResult<Record<string, unknown>> {
|
|
433
|
+
const errors: StandardIssue[] = [];
|
|
434
|
+
const validated: Record<string, unknown> = {};
|
|
435
|
+
|
|
436
|
+
if (typeof data !== "object" || data === null) {
|
|
437
|
+
return {
|
|
438
|
+
issues: [{ message: "Input must be an object" }],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const obj = data as Record<string, unknown>;
|
|
443
|
+
|
|
444
|
+
for (const [key, validator] of Object.entries(this.definition)) {
|
|
445
|
+
if (typeof validator === "function") {
|
|
446
|
+
const result = validator(obj[key]);
|
|
447
|
+
if (!result.valid) {
|
|
448
|
+
errors.push({
|
|
449
|
+
message: result.error || `Validation failed for ${key}`,
|
|
450
|
+
path: [key],
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
// Use coerced value if provided, otherwise use original value
|
|
454
|
+
validated[key] = result.value !== undefined ? result.value : obj[key];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (errors.length > 0) {
|
|
460
|
+
return { issues: errors };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return { value: validated };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Validate data asynchronously
|
|
468
|
+
* Currently same as sync, but allows for async validators in future
|
|
469
|
+
*/
|
|
470
|
+
async validate(
|
|
471
|
+
data: unknown,
|
|
472
|
+
): Promise<StandardResult<Record<string, unknown>>> {
|
|
473
|
+
return this.validateSync(data);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Implement Standard Schema interface for compatibility with @buenojs/bueno validators
|
|
478
|
+
*/
|
|
479
|
+
get ["~standard"]() {
|
|
480
|
+
return {
|
|
481
|
+
validate: (data: unknown) => this.validateSync(data),
|
|
482
|
+
version: 1,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Create a schema object
|
|
489
|
+
* Alias for Schema.object() for convenience
|
|
490
|
+
*/
|
|
491
|
+
export function schema<T extends SchemaDefinition>(definition: T): Schema {
|
|
492
|
+
return Schema.object(definition);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============= Environment Variable Schemas =============
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Standard environment variable schema for Bueno Framework
|
|
499
|
+
*/
|
|
500
|
+
export const envSchema = Schema.object({
|
|
501
|
+
// Required variables
|
|
502
|
+
NODE_ENV: Fields.env.required(),
|
|
503
|
+
PORT: Fields.env.port(),
|
|
504
|
+
HOST: Fields.string({ optional: true }),
|
|
505
|
+
|
|
506
|
+
// Database
|
|
507
|
+
DATABASE_URL: Fields.env.databaseUrl({ optional: true }),
|
|
508
|
+
DATABASE_POOL_SIZE: Fields.env.number({ min: 1, max: 100, optional: true }),
|
|
509
|
+
|
|
510
|
+
// Cache
|
|
511
|
+
REDIS_URL: Fields.env.url({ optional: true }),
|
|
512
|
+
CACHE_DRIVER: Fields.enum(["memory", "redis", "none"], { optional: true }),
|
|
513
|
+
CACHE_TTL: Fields.env.number({ min: 1, optional: true }),
|
|
514
|
+
|
|
515
|
+
// Logging
|
|
516
|
+
LOG_LEVEL: Fields.enum(["error", "warn", "info", "debug", "trace"], {
|
|
517
|
+
optional: true,
|
|
518
|
+
}),
|
|
519
|
+
LOG_PRETTY: Fields.env.boolean({ optional: true }),
|
|
520
|
+
|
|
521
|
+
// Health
|
|
522
|
+
HEALTH_ENABLED: Fields.env.boolean({ optional: true }),
|
|
523
|
+
|
|
524
|
+
// Metrics
|
|
525
|
+
METRICS_ENABLED: Fields.env.boolean({ optional: true }),
|
|
526
|
+
|
|
527
|
+
// Telemetry
|
|
528
|
+
TELEMETRY_ENABLED: Fields.env.boolean({ optional: true }),
|
|
529
|
+
TELEMETRY_SERVICE_NAME: Fields.string({ optional: true }),
|
|
530
|
+
TELEMETRY_ENDPOINT: Fields.env.url({ optional: true }),
|
|
531
|
+
|
|
532
|
+
// Frontend
|
|
533
|
+
FRONTEND_DEV_SERVER: Fields.env.boolean({ optional: true }),
|
|
534
|
+
FRONTEND_HMR: Fields.env.boolean({ optional: true }),
|
|
535
|
+
FRONTEND_PORT: Fields.env.port({ optional: true }),
|
|
536
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import {
|
|
4
3
|
Router,
|
|
5
4
|
Context,
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
Database,
|
|
12
11
|
createServer,
|
|
13
12
|
} from '../../src';
|
|
13
|
+
import { Schema, Fields } from '../../src/validation/schemas';
|
|
14
14
|
|
|
15
15
|
// ============= Integration Tests =============
|
|
16
16
|
|
|
@@ -107,9 +107,9 @@ describe('Integration Tests', () => {
|
|
|
107
107
|
|
|
108
108
|
describe('Validation Integration', () => {
|
|
109
109
|
test('should validate request body', async () => {
|
|
110
|
-
const schema =
|
|
111
|
-
email:
|
|
112
|
-
age:
|
|
110
|
+
const schema = Schema.object({
|
|
111
|
+
email: Fields.string({ email: true }),
|
|
112
|
+
age: Fields.number({ min: 0 }),
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
const validator = createValidator({ body: schema });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
-
import { Database, detectDriver, createConnection, QueryBuilder
|
|
2
|
+
import { Database, detectDriver, createConnection, QueryBuilder } from '../../src/database';
|
|
3
|
+
import { query } from '../../src/database/orm/builder';
|
|
3
4
|
|
|
4
5
|
describe('Database', () => {
|
|
5
6
|
// Use SQLite for testing (no external dependencies)
|
|
@@ -102,77 +103,6 @@ describe('Database', () => {
|
|
|
102
103
|
});
|
|
103
104
|
});
|
|
104
105
|
|
|
105
|
-
describe('QueryBuilder', () => {
|
|
106
|
-
let db: Database;
|
|
107
|
-
let users: QueryBuilder<{ id: number; name: string; email: string }>;
|
|
108
|
-
|
|
109
|
-
beforeEach(async () => {
|
|
110
|
-
db = new Database({ url: testDbPath });
|
|
111
|
-
await db.connect();
|
|
112
|
-
|
|
113
|
-
await db.raw(`
|
|
114
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
115
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
|
-
name TEXT NOT NULL,
|
|
117
|
-
email TEXT UNIQUE
|
|
118
|
-
)
|
|
119
|
-
`);
|
|
120
|
-
|
|
121
|
-
users = table<{ id: number; name: string; email: string }>(db, 'users');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
afterEach(async () => {
|
|
125
|
-
await db.close();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('should insert and find by id', async () => {
|
|
129
|
-
const inserted = await users.insert({ name: 'John', email: 'john@example.com' });
|
|
130
|
-
expect(inserted.name).toBe('John');
|
|
131
|
-
|
|
132
|
-
const found = await users.findById(inserted.id);
|
|
133
|
-
expect(found?.name).toBe('John');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('should count rows', async () => {
|
|
137
|
-
await users.insert({ name: 'John', email: 'john@example.com' });
|
|
138
|
-
await users.insert({ name: 'Jane', email: 'jane@example.com' });
|
|
139
|
-
|
|
140
|
-
const count = await users.count();
|
|
141
|
-
expect(count).toBe(2);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('should delete by id', async () => {
|
|
145
|
-
const inserted = await users.insert({ name: 'John', email: 'john@example.com' });
|
|
146
|
-
|
|
147
|
-
const deleted = await users.deleteById(inserted.id);
|
|
148
|
-
expect(deleted).toBe(true);
|
|
149
|
-
|
|
150
|
-
const found = await users.findById(inserted.id);
|
|
151
|
-
expect(found).toBeNull();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('should update by id', async () => {
|
|
155
|
-
const inserted = await users.insert({ name: 'John', email: 'john@example.com' });
|
|
156
|
-
|
|
157
|
-
const updated = await users.updateById(inserted.id, { name: 'Johnny' });
|
|
158
|
-
expect(updated?.name).toBe('Johnny');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('should paginate results', async () => {
|
|
162
|
-
for (let i = 0; i < 25; i++) {
|
|
163
|
-
await users.insert({ name: `User${i}`, email: `user${i}@example.com` });
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const page1 = await users.paginate(1, 10);
|
|
167
|
-
expect(page1.data.length).toBe(10);
|
|
168
|
-
expect(page1.total).toBe(25);
|
|
169
|
-
expect(page1.totalPages).toBe(3);
|
|
170
|
-
|
|
171
|
-
const page3 = await users.paginate(3, 10);
|
|
172
|
-
expect(page3.data.length).toBe(5);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
106
|
describe('createConnection', () => {
|
|
177
107
|
test('should create and connect to database', async () => {
|
|
178
108
|
const db = await createConnection({ url: ':memory:' });
|