create-phoenixjs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +196 -0
- package/package.json +31 -0
- package/template/README.md +62 -0
- package/template/app/controllers/ExampleController.ts +61 -0
- package/template/artisan +2 -0
- package/template/bootstrap/app.ts +44 -0
- package/template/bunfig.toml +7 -0
- package/template/config/database.ts +25 -0
- package/template/config/plugins.ts +7 -0
- package/template/config/security.ts +158 -0
- package/template/framework/cli/Command.ts +17 -0
- package/template/framework/cli/ConsoleApplication.ts +55 -0
- package/template/framework/cli/artisan.ts +16 -0
- package/template/framework/cli/commands/MakeControllerCommand.ts +41 -0
- package/template/framework/cli/commands/MakeMiddlewareCommand.ts +41 -0
- package/template/framework/cli/commands/MakeModelCommand.ts +36 -0
- package/template/framework/cli/commands/MakeValidatorCommand.ts +42 -0
- package/template/framework/controller/Controller.ts +222 -0
- package/template/framework/core/Application.ts +208 -0
- package/template/framework/core/Container.ts +100 -0
- package/template/framework/core/Kernel.ts +297 -0
- package/template/framework/database/DatabaseAdapter.ts +18 -0
- package/template/framework/database/PrismaAdapter.ts +65 -0
- package/template/framework/database/SqlAdapter.ts +117 -0
- package/template/framework/gateway/Gateway.ts +109 -0
- package/template/framework/gateway/GatewayManager.ts +150 -0
- package/template/framework/gateway/WebSocketAdapter.ts +159 -0
- package/template/framework/gateway/WebSocketGateway.ts +182 -0
- package/template/framework/http/Request.ts +608 -0
- package/template/framework/http/Response.ts +525 -0
- package/template/framework/http/Server.ts +161 -0
- package/template/framework/http/UploadedFile.ts +145 -0
- package/template/framework/middleware/Middleware.ts +50 -0
- package/template/framework/middleware/Pipeline.ts +89 -0
- package/template/framework/plugin/Plugin.ts +26 -0
- package/template/framework/plugin/PluginManager.ts +61 -0
- package/template/framework/routing/RouteRegistry.ts +185 -0
- package/template/framework/routing/Router.ts +280 -0
- package/template/framework/security/CorsMiddleware.ts +151 -0
- package/template/framework/security/CsrfMiddleware.ts +121 -0
- package/template/framework/security/HelmetMiddleware.ts +138 -0
- package/template/framework/security/InputSanitizerMiddleware.ts +134 -0
- package/template/framework/security/RateLimiterMiddleware.ts +189 -0
- package/template/framework/security/SecurityManager.ts +128 -0
- package/template/framework/validation/Validator.ts +482 -0
- package/template/package.json +24 -0
- package/template/routes/api.ts +56 -0
- package/template/server.ts +29 -0
- package/template/tsconfig.json +49 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Base Validator
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for all validators.
|
|
5
|
+
* Provides a rule-based validation engine.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validation result returned by validate()
|
|
10
|
+
*/
|
|
11
|
+
export interface ValidationResult {
|
|
12
|
+
/** Whether all validations passed */
|
|
13
|
+
passes: boolean;
|
|
14
|
+
/** Whether any validation failed */
|
|
15
|
+
fails: boolean;
|
|
16
|
+
/** Validation errors keyed by field name */
|
|
17
|
+
errors: Record<string, string[]>;
|
|
18
|
+
/** Validated data (only if passes) */
|
|
19
|
+
validated: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validation rules format
|
|
24
|
+
* Key is the field name, value is an array of rule strings
|
|
25
|
+
*
|
|
26
|
+
* Example:
|
|
27
|
+
* {
|
|
28
|
+
* name: ['required', 'string', 'min:2', 'max:100'],
|
|
29
|
+
* email: ['required', 'email'],
|
|
30
|
+
* age: ['number', 'min:0', 'max:150'],
|
|
31
|
+
* role: ['required', 'in:admin,user,guest']
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
export type ValidationRules = Record<string, string[]>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom validation messages
|
|
38
|
+
*/
|
|
39
|
+
export type ValidationMessages = Record<string, string>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Rule handler function type
|
|
43
|
+
*/
|
|
44
|
+
type RuleHandler = (
|
|
45
|
+
field: string,
|
|
46
|
+
value: unknown,
|
|
47
|
+
param: string | undefined,
|
|
48
|
+
data: Record<string, unknown>
|
|
49
|
+
) => string | null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Base Validator class
|
|
53
|
+
*
|
|
54
|
+
* All application validators should extend this class.
|
|
55
|
+
* Provides a generic rule-based validation engine.
|
|
56
|
+
*/
|
|
57
|
+
export abstract class Validator {
|
|
58
|
+
/**
|
|
59
|
+
* Define validation rules for fields
|
|
60
|
+
* Override this in subclasses
|
|
61
|
+
*/
|
|
62
|
+
abstract rules(): ValidationRules;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Custom error messages (optional override)
|
|
66
|
+
*/
|
|
67
|
+
messages(): ValidationMessages {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Custom attribute names (optional override)
|
|
73
|
+
*/
|
|
74
|
+
attributes(): Record<string, string> {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate the given data against the rules
|
|
80
|
+
*/
|
|
81
|
+
validate(data: Record<string, unknown>): ValidationResult {
|
|
82
|
+
const rules = this.rules();
|
|
83
|
+
const customMessages = this.messages();
|
|
84
|
+
const customAttributes = this.attributes();
|
|
85
|
+
const errors: Record<string, string[]> = {};
|
|
86
|
+
const validated: Record<string, unknown> = {};
|
|
87
|
+
|
|
88
|
+
for (const [field, fieldRules] of Object.entries(rules)) {
|
|
89
|
+
const value = this.getNestedValue(data, field);
|
|
90
|
+
const fieldErrors: string[] = [];
|
|
91
|
+
const displayName = customAttributes[field] || this.humanize(field);
|
|
92
|
+
|
|
93
|
+
for (const rule of fieldRules) {
|
|
94
|
+
const [ruleName, ruleParam] = this.parseRule(rule);
|
|
95
|
+
const handler = this.getHandler(ruleName);
|
|
96
|
+
|
|
97
|
+
if (handler) {
|
|
98
|
+
const errorMessage = handler(field, value, ruleParam, data);
|
|
99
|
+
if (errorMessage) {
|
|
100
|
+
// Check for custom message
|
|
101
|
+
const customKey = `${field}.${ruleName}`;
|
|
102
|
+
const message = customMessages[customKey]
|
|
103
|
+
|| customMessages[ruleName]
|
|
104
|
+
|| errorMessage.replace(':attribute', displayName);
|
|
105
|
+
fieldErrors.push(message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (fieldErrors.length > 0) {
|
|
111
|
+
errors[field] = fieldErrors;
|
|
112
|
+
} else if (value !== undefined) {
|
|
113
|
+
validated[field] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const passes = Object.keys(errors).length === 0;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
passes,
|
|
121
|
+
fails: !passes,
|
|
122
|
+
errors,
|
|
123
|
+
validated: passes ? validated : {},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse a rule string into name and parameter
|
|
129
|
+
*/
|
|
130
|
+
private parseRule(rule: string): [string, string | undefined] {
|
|
131
|
+
const colonIndex = rule.indexOf(':');
|
|
132
|
+
if (colonIndex === -1) {
|
|
133
|
+
return [rule, undefined];
|
|
134
|
+
}
|
|
135
|
+
return [rule.substring(0, colonIndex), rule.substring(colonIndex + 1)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get nested value from object using dot notation
|
|
140
|
+
*/
|
|
141
|
+
private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
142
|
+
const keys = path.split('.');
|
|
143
|
+
let current: unknown = obj;
|
|
144
|
+
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
current = (current as Record<string, unknown>)[key];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Convert field name to human-readable format
|
|
157
|
+
*/
|
|
158
|
+
private humanize(field: string): string {
|
|
159
|
+
return field
|
|
160
|
+
.replace(/([A-Z])/g, ' $1')
|
|
161
|
+
.replace(/[_-]/g, ' ')
|
|
162
|
+
.toLowerCase()
|
|
163
|
+
.trim()
|
|
164
|
+
.replace(/^\w/, (c) => c.toUpperCase());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get the handler for a rule
|
|
169
|
+
*/
|
|
170
|
+
private getHandler(ruleName: string): RuleHandler | null {
|
|
171
|
+
const handlers: Record<string, RuleHandler> = {
|
|
172
|
+
required: this.validateRequired.bind(this),
|
|
173
|
+
string: this.validateString.bind(this),
|
|
174
|
+
number: this.validateNumber.bind(this),
|
|
175
|
+
boolean: this.validateBoolean.bind(this),
|
|
176
|
+
array: this.validateArray.bind(this),
|
|
177
|
+
object: this.validateObject.bind(this),
|
|
178
|
+
email: this.validateEmail.bind(this),
|
|
179
|
+
url: this.validateUrl.bind(this),
|
|
180
|
+
min: this.validateMin.bind(this),
|
|
181
|
+
max: this.validateMax.bind(this),
|
|
182
|
+
between: this.validateBetween.bind(this),
|
|
183
|
+
in: this.validateIn.bind(this),
|
|
184
|
+
notIn: this.validateNotIn.bind(this),
|
|
185
|
+
regex: this.validateRegex.bind(this),
|
|
186
|
+
alpha: this.validateAlpha.bind(this),
|
|
187
|
+
alphaNum: this.validateAlphaNum.bind(this),
|
|
188
|
+
alphaDash: this.validateAlphaDash.bind(this),
|
|
189
|
+
numeric: this.validateNumeric.bind(this),
|
|
190
|
+
integer: this.validateInteger.bind(this),
|
|
191
|
+
date: this.validateDate.bind(this),
|
|
192
|
+
uuid: this.validateUuid.bind(this),
|
|
193
|
+
confirmed: this.validateConfirmed.bind(this),
|
|
194
|
+
same: this.validateSame.bind(this),
|
|
195
|
+
different: this.validateDifferent.bind(this),
|
|
196
|
+
nullable: this.validateNullable.bind(this),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return handlers[ruleName] || null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ==========================================
|
|
203
|
+
// Rule Validators
|
|
204
|
+
// ==========================================
|
|
205
|
+
|
|
206
|
+
private validateRequired(field: string, value: unknown): string | null {
|
|
207
|
+
if (value === undefined || value === null || value === '') {
|
|
208
|
+
return 'The :attribute field is required.';
|
|
209
|
+
}
|
|
210
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
211
|
+
return 'The :attribute field is required.';
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private validateString(field: string, value: unknown): string | null {
|
|
217
|
+
if (value === undefined || value === null) return null;
|
|
218
|
+
if (typeof value !== 'string') {
|
|
219
|
+
return 'The :attribute must be a string.';
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private validateNumber(field: string, value: unknown): string | null {
|
|
225
|
+
if (value === undefined || value === null) return null;
|
|
226
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
227
|
+
return 'The :attribute must be a number.';
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private validateBoolean(field: string, value: unknown): string | null {
|
|
233
|
+
if (value === undefined || value === null) return null;
|
|
234
|
+
if (typeof value !== 'boolean' && value !== 0 && value !== 1 && value !== '0' && value !== '1') {
|
|
235
|
+
return 'The :attribute must be true or false.';
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private validateArray(field: string, value: unknown): string | null {
|
|
241
|
+
if (value === undefined || value === null) return null;
|
|
242
|
+
if (!Array.isArray(value)) {
|
|
243
|
+
return 'The :attribute must be an array.';
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private validateObject(field: string, value: unknown): string | null {
|
|
249
|
+
if (value === undefined || value === null) return null;
|
|
250
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
251
|
+
return 'The :attribute must be an object.';
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private validateEmail(field: string, value: unknown): string | null {
|
|
257
|
+
if (value === undefined || value === null || value === '') return null;
|
|
258
|
+
if (typeof value !== 'string') {
|
|
259
|
+
return 'The :attribute must be a valid email address.';
|
|
260
|
+
}
|
|
261
|
+
// RFC 5322 compliant email regex (simplified)
|
|
262
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
263
|
+
if (!emailRegex.test(value)) {
|
|
264
|
+
return 'The :attribute must be a valid email address.';
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private validateUrl(field: string, value: unknown): string | null {
|
|
270
|
+
if (value === undefined || value === null || value === '') return null;
|
|
271
|
+
if (typeof value !== 'string') {
|
|
272
|
+
return 'The :attribute must be a valid URL.';
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
new URL(value);
|
|
276
|
+
return null;
|
|
277
|
+
} catch {
|
|
278
|
+
return 'The :attribute must be a valid URL.';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private validateMin(field: string, value: unknown, param: string | undefined): string | null {
|
|
283
|
+
if (value === undefined || value === null) return null;
|
|
284
|
+
const min = Number(param);
|
|
285
|
+
if (isNaN(min)) return null;
|
|
286
|
+
|
|
287
|
+
if (typeof value === 'string') {
|
|
288
|
+
if (value.length < min) {
|
|
289
|
+
return `The :attribute must be at least ${min} characters.`;
|
|
290
|
+
}
|
|
291
|
+
} else if (typeof value === 'number') {
|
|
292
|
+
if (value < min) {
|
|
293
|
+
return `The :attribute must be at least ${min}.`;
|
|
294
|
+
}
|
|
295
|
+
} else if (Array.isArray(value)) {
|
|
296
|
+
if (value.length < min) {
|
|
297
|
+
return `The :attribute must have at least ${min} items.`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private validateMax(field: string, value: unknown, param: string | undefined): string | null {
|
|
304
|
+
if (value === undefined || value === null) return null;
|
|
305
|
+
const max = Number(param);
|
|
306
|
+
if (isNaN(max)) return null;
|
|
307
|
+
|
|
308
|
+
if (typeof value === 'string') {
|
|
309
|
+
if (value.length > max) {
|
|
310
|
+
return `The :attribute must not be greater than ${max} characters.`;
|
|
311
|
+
}
|
|
312
|
+
} else if (typeof value === 'number') {
|
|
313
|
+
if (value > max) {
|
|
314
|
+
return `The :attribute must not be greater than ${max}.`;
|
|
315
|
+
}
|
|
316
|
+
} else if (Array.isArray(value)) {
|
|
317
|
+
if (value.length > max) {
|
|
318
|
+
return `The :attribute must not have more than ${max} items.`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private validateBetween(field: string, value: unknown, param: string | undefined): string | null {
|
|
325
|
+
if (value === undefined || value === null) return null;
|
|
326
|
+
if (!param) return null;
|
|
327
|
+
const [minStr, maxStr] = param.split(',');
|
|
328
|
+
const min = Number(minStr);
|
|
329
|
+
const max = Number(maxStr);
|
|
330
|
+
if (isNaN(min) || isNaN(max)) return null;
|
|
331
|
+
|
|
332
|
+
if (typeof value === 'string') {
|
|
333
|
+
if (value.length < min || value.length > max) {
|
|
334
|
+
return `The :attribute must be between ${min} and ${max} characters.`;
|
|
335
|
+
}
|
|
336
|
+
} else if (typeof value === 'number') {
|
|
337
|
+
if (value < min || value > max) {
|
|
338
|
+
return `The :attribute must be between ${min} and ${max}.`;
|
|
339
|
+
}
|
|
340
|
+
} else if (Array.isArray(value)) {
|
|
341
|
+
if (value.length < min || value.length > max) {
|
|
342
|
+
return `The :attribute must have between ${min} and ${max} items.`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private validateIn(field: string, value: unknown, param: string | undefined): string | null {
|
|
349
|
+
if (value === undefined || value === null) return null;
|
|
350
|
+
if (!param) return null;
|
|
351
|
+
const allowed = param.split(',');
|
|
352
|
+
if (!allowed.includes(String(value))) {
|
|
353
|
+
return `The selected :attribute is invalid.`;
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private validateNotIn(field: string, value: unknown, param: string | undefined): string | null {
|
|
359
|
+
if (value === undefined || value === null) return null;
|
|
360
|
+
if (!param) return null;
|
|
361
|
+
const disallowed = param.split(',');
|
|
362
|
+
if (disallowed.includes(String(value))) {
|
|
363
|
+
return `The selected :attribute is invalid.`;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private validateRegex(field: string, value: unknown, param: string | undefined): string | null {
|
|
369
|
+
if (value === undefined || value === null || value === '') return null;
|
|
370
|
+
if (!param) return null;
|
|
371
|
+
if (typeof value !== 'string') {
|
|
372
|
+
return 'The :attribute format is invalid.';
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const regex = new RegExp(param);
|
|
376
|
+
if (!regex.test(value)) {
|
|
377
|
+
return 'The :attribute format is invalid.';
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
return 'The :attribute format is invalid.';
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private validateAlpha(field: string, value: unknown): string | null {
|
|
386
|
+
if (value === undefined || value === null || value === '') return null;
|
|
387
|
+
if (typeof value !== 'string' || !/^[a-zA-Z]+$/.test(value)) {
|
|
388
|
+
return 'The :attribute must only contain letters.';
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private validateAlphaNum(field: string, value: unknown): string | null {
|
|
394
|
+
if (value === undefined || value === null || value === '') return null;
|
|
395
|
+
if (typeof value !== 'string' || !/^[a-zA-Z0-9]+$/.test(value)) {
|
|
396
|
+
return 'The :attribute must only contain letters and numbers.';
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private validateAlphaDash(field: string, value: unknown): string | null {
|
|
402
|
+
if (value === undefined || value === null || value === '') return null;
|
|
403
|
+
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
404
|
+
return 'The :attribute must only contain letters, numbers, dashes and underscores.';
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private validateNumeric(field: string, value: unknown): string | null {
|
|
410
|
+
if (value === undefined || value === null || value === '') return null;
|
|
411
|
+
if (isNaN(Number(value))) {
|
|
412
|
+
return 'The :attribute must be a number.';
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private validateInteger(field: string, value: unknown): string | null {
|
|
418
|
+
if (value === undefined || value === null) return null;
|
|
419
|
+
const num = Number(value);
|
|
420
|
+
if (isNaN(num) || !Number.isInteger(num)) {
|
|
421
|
+
return 'The :attribute must be an integer.';
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private validateDate(field: string, value: unknown): string | null {
|
|
427
|
+
if (value === undefined || value === null || value === '') return null;
|
|
428
|
+
const date = new Date(value as string | number);
|
|
429
|
+
if (isNaN(date.getTime())) {
|
|
430
|
+
return 'The :attribute must be a valid date.';
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private validateUuid(field: string, value: unknown): string | null {
|
|
436
|
+
if (value === undefined || value === null || value === '') return null;
|
|
437
|
+
if (typeof value !== 'string') {
|
|
438
|
+
return 'The :attribute must be a valid UUID.';
|
|
439
|
+
}
|
|
440
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
441
|
+
if (!uuidRegex.test(value)) {
|
|
442
|
+
return 'The :attribute must be a valid UUID.';
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private validateConfirmed(field: string, value: unknown, param: string | undefined, data: Record<string, unknown>): string | null {
|
|
448
|
+
if (value === undefined || value === null) return null;
|
|
449
|
+
const confirmationField = `${field}_confirmation`;
|
|
450
|
+
const confirmationValue = this.getNestedValue(data, confirmationField);
|
|
451
|
+
if (value !== confirmationValue) {
|
|
452
|
+
return 'The :attribute confirmation does not match.';
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private validateSame(field: string, value: unknown, param: string | undefined, data: Record<string, unknown>): string | null {
|
|
458
|
+
if (value === undefined || value === null) return null;
|
|
459
|
+
if (!param) return null;
|
|
460
|
+
const otherValue = this.getNestedValue(data, param);
|
|
461
|
+
if (value !== otherValue) {
|
|
462
|
+
return `The :attribute and ${param} must match.`;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private validateDifferent(field: string, value: unknown, param: string | undefined, data: Record<string, unknown>): string | null {
|
|
468
|
+
if (value === undefined || value === null) return null;
|
|
469
|
+
if (!param) return null;
|
|
470
|
+
const otherValue = this.getNestedValue(data, param);
|
|
471
|
+
if (value === otherValue) {
|
|
472
|
+
return `The :attribute and ${param} must be different.`;
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private validateNullable(): string | null {
|
|
478
|
+
// Nullable is a special rule that allows null/undefined values
|
|
479
|
+
// It doesn't produce errors, it just signals that null is allowed
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-phoenixjs-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A PhoenixJS API project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "bun run --watch server.ts",
|
|
9
|
+
"start": "bun run server.ts",
|
|
10
|
+
"test": "bun test"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"api",
|
|
14
|
+
"typescript",
|
|
15
|
+
"bun",
|
|
16
|
+
"phoenixjs"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "latest",
|
|
22
|
+
"typescript": "^5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Router } from '@framework/routing/Router';
|
|
2
|
+
import { FrameworkResponse as Response } from '@framework/http/Response';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* API Routes
|
|
6
|
+
*
|
|
7
|
+
* Define your API routes here. These routes are loaded by the kernel
|
|
8
|
+
* and all routes here will have the /api prefix.
|
|
9
|
+
*/
|
|
10
|
+
export function registerRoutes() {
|
|
11
|
+
// Health check endpoint
|
|
12
|
+
Router.get('/health', () => {
|
|
13
|
+
return Response.json({
|
|
14
|
+
status: 'ok',
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Welcome endpoint
|
|
20
|
+
Router.get('/hello', () => {
|
|
21
|
+
return Response.json({
|
|
22
|
+
message: 'Welcome to PhoenixJS! 🚀',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Example API group with routes
|
|
27
|
+
Router.group({ prefix: '/api' }, () => {
|
|
28
|
+
// Ping endpoint
|
|
29
|
+
Router.get('/ping', () => Response.json({ message: 'pong' }));
|
|
30
|
+
|
|
31
|
+
// Example with route parameter
|
|
32
|
+
Router.get('/users/:id', () => {
|
|
33
|
+
return Response.json({
|
|
34
|
+
user: { id: '1', name: 'John Doe' },
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Example POST route (use Controller for real implementations)
|
|
39
|
+
Router.post('/users', () => {
|
|
40
|
+
return Response.json({
|
|
41
|
+
message: 'User created',
|
|
42
|
+
}, 201);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Route Groups Example
|
|
48
|
+
*
|
|
49
|
+
* You can use Controllers with the 'ControllerName@method' syntax:
|
|
50
|
+
*
|
|
51
|
+
* Router.group({ prefix: '/v1', middleware: [] }, () => {
|
|
52
|
+
* Router.get('/products', 'ProductController@index');
|
|
53
|
+
* Router.post('/products', 'ProductController@store');
|
|
54
|
+
* });
|
|
55
|
+
*/
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Server Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the application.
|
|
5
|
+
* Run with: bun run server.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { app, kernel } from '@/bootstrap/app';
|
|
9
|
+
import { Server } from '@framework/http/Server';
|
|
10
|
+
|
|
11
|
+
// Boot the application
|
|
12
|
+
app.boot();
|
|
13
|
+
|
|
14
|
+
// Create and start the server
|
|
15
|
+
const server = new Server(kernel);
|
|
16
|
+
server.start();
|
|
17
|
+
|
|
18
|
+
// Handle graceful shutdown
|
|
19
|
+
process.on('SIGINT', () => {
|
|
20
|
+
console.log('\nShutting down...');
|
|
21
|
+
server.stop();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
process.on('SIGTERM', () => {
|
|
26
|
+
console.log('\nShutting down...');
|
|
27
|
+
server.stop();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"declaration": false,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"lib": [
|
|
16
|
+
"ESNext"
|
|
17
|
+
],
|
|
18
|
+
"types": [
|
|
19
|
+
"bun-types"
|
|
20
|
+
],
|
|
21
|
+
"baseUrl": ".",
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/*": [
|
|
24
|
+
"./*"
|
|
25
|
+
],
|
|
26
|
+
"@framework/*": [
|
|
27
|
+
"framework/*"
|
|
28
|
+
],
|
|
29
|
+
"@app/*": [
|
|
30
|
+
"app/*"
|
|
31
|
+
],
|
|
32
|
+
"@config/*": [
|
|
33
|
+
"config/*"
|
|
34
|
+
],
|
|
35
|
+
"@routes/*": [
|
|
36
|
+
"routes/*"
|
|
37
|
+
],
|
|
38
|
+
"@database/*": [
|
|
39
|
+
"database/*"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"include": [
|
|
44
|
+
"**/*.ts"
|
|
45
|
+
],
|
|
46
|
+
"exclude": [
|
|
47
|
+
"node_modules"
|
|
48
|
+
]
|
|
49
|
+
}
|