bunsane 0.1.0 → 0.1.2
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/.github/workflows/deploy-docs.yml +57 -0
- package/LICENSE.md +1 -1
- package/README.md +2 -28
- package/TODO.md +8 -1
- package/bun.lock +3 -0
- package/config/upload.config.ts +135 -0
- package/core/App.ts +168 -4
- package/core/ArcheType.ts +122 -0
- package/core/BatchLoader.ts +100 -0
- package/core/ComponentRegistry.ts +4 -3
- package/core/Components.ts +2 -2
- package/core/Decorators.ts +15 -8
- package/core/Entity.ts +193 -14
- package/core/EntityCache.ts +15 -0
- package/core/EntityHookManager.ts +855 -0
- package/core/EntityManager.ts +12 -2
- package/core/ErrorHandler.ts +64 -7
- package/core/FileValidator.ts +284 -0
- package/core/Query.ts +503 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +89 -0
- package/core/SchedulerManager.ts +710 -0
- package/core/UploadManager.ts +261 -0
- package/core/components/UploadComponent.ts +206 -0
- package/core/decorators/EntityHooks.ts +190 -0
- package/core/decorators/ScheduledTask.ts +83 -0
- package/core/events/EntityLifecycleEvents.ts +177 -0
- package/core/processors/ImageProcessor.ts +423 -0
- package/core/storage/LocalStorageProvider.ts +290 -0
- package/core/storage/StorageProvider.ts +112 -0
- package/database/DatabaseHelper.ts +183 -58
- package/database/index.ts +5 -5
- package/database/sqlHelpers.ts +7 -0
- package/docs/README.md +149 -0
- package/docs/_coverpage.md +36 -0
- package/docs/_sidebar.md +23 -0
- package/docs/api/core.md +568 -0
- package/docs/api/hooks.md +554 -0
- package/docs/api/index.md +222 -0
- package/docs/api/query.md +678 -0
- package/docs/api/service.md +744 -0
- package/docs/core-concepts/archetypes.md +512 -0
- package/docs/core-concepts/components.md +498 -0
- package/docs/core-concepts/entity.md +314 -0
- package/docs/core-concepts/hooks.md +683 -0
- package/docs/core-concepts/query.md +588 -0
- package/docs/core-concepts/services.md +647 -0
- package/docs/examples/code-examples.md +425 -0
- package/docs/getting-started.md +337 -0
- package/docs/index.html +97 -0
- package/gql/Generator.ts +58 -35
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +65 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +19 -10
- package/rest/Generator.ts +3 -0
- package/rest/index.ts +22 -0
- package/service/Service.ts +1 -1
- package/service/ServiceRegistry.ts +10 -6
- package/service/index.ts +12 -1
- package/tests/bench/insert.bench.ts +59 -0
- package/tests/bench/relations.bench.ts +269 -0
- package/tests/bench/sorting.bench.ts +415 -0
- package/tests/component-hooks.test.ts +1409 -0
- package/tests/component.test.ts +338 -0
- package/tests/errorHandling.test.ts +155 -0
- package/tests/hooks.test.ts +666 -0
- package/tests/query-sorting.test.ts +101 -0
- package/tests/relations.test.ts +169 -0
- package/tests/scheduler.test.ts +724 -0
- package/tsconfig.json +35 -34
- package/types/graphql.types.ts +87 -0
- package/types/hooks.types.ts +141 -0
- package/types/scheduler.types.ts +165 -0
- package/types/upload.types.ts +184 -0
- package/upload/index.ts +140 -0
- package/utils/UploadHelper.ts +305 -0
- package/utils/cronParser.ts +366 -0
- package/utils/errorMessages.ts +151 -0
- package/core/Events.ts +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron Expression Parser for BunSane Scheduler
|
|
3
|
+
* Supports standard cron expressions with 5 or 6 fields
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CronFields {
|
|
7
|
+
minute: number[];
|
|
8
|
+
hour: number[];
|
|
9
|
+
dayOfMonth: number[];
|
|
10
|
+
month: number[];
|
|
11
|
+
dayOfWeek: number[];
|
|
12
|
+
// Optional seconds field for 6-field expressions
|
|
13
|
+
second?: number[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CronValidationResult {
|
|
17
|
+
isValid: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
fields?: CronFields;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class CronParser {
|
|
23
|
+
/**
|
|
24
|
+
* Parse a cron expression into its component fields
|
|
25
|
+
* @param expression Standard cron expression (e.g., "0 2 * * 1" or "*\/5 * * * *")
|
|
26
|
+
* @returns Parsed cron fields or validation error
|
|
27
|
+
*/
|
|
28
|
+
static parse(expression: string): CronValidationResult {
|
|
29
|
+
if (!expression || typeof expression !== 'string') {
|
|
30
|
+
return { isValid: false, error: 'Expression must be a non-empty string' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parts = expression.trim().split(/\s+/);
|
|
34
|
+
if (parts.length !== 5 && parts.length !== 6) {
|
|
35
|
+
return { isValid: false, error: 'Cron expression must have 5 or 6 fields' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isSixField = parts.length === 6;
|
|
39
|
+
const fields: CronFields = {
|
|
40
|
+
minute: [],
|
|
41
|
+
hour: [],
|
|
42
|
+
dayOfMonth: [],
|
|
43
|
+
month: [],
|
|
44
|
+
dayOfWeek: []
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (isSixField) {
|
|
48
|
+
fields.second = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse each field
|
|
52
|
+
const fieldNames = isSixField
|
|
53
|
+
? ['second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek']
|
|
54
|
+
: ['minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'];
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < parts.length; i++) {
|
|
57
|
+
const fieldName = fieldNames[i]!;
|
|
58
|
+
const fieldValue = parts[i]!;
|
|
59
|
+
const validation = CronParser.validateField(fieldName, fieldValue);
|
|
60
|
+
|
|
61
|
+
if (!validation.isValid) {
|
|
62
|
+
return { isValid: false, error: `${fieldName}: ${validation.error}` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
(fields as any)[fieldName] = validation.values;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { isValid: true, fields };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate a single cron field
|
|
73
|
+
*/
|
|
74
|
+
private static validateField(fieldName: string, value: string): { isValid: boolean; error?: string; values: number[] } {
|
|
75
|
+
if (value === '*') {
|
|
76
|
+
return { isValid: true, values: CronParser.getAllValuesForField(fieldName) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value.includes('/')) {
|
|
80
|
+
return CronParser.parseStepValue(fieldName, value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (value.includes('-')) {
|
|
84
|
+
return CronParser.parseRangeValue(fieldName, value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (value.includes(',')) {
|
|
88
|
+
return CronParser.parseListValue(fieldName, value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Single value
|
|
92
|
+
const numValue = parseInt(value, 10);
|
|
93
|
+
if (isNaN(numValue)) {
|
|
94
|
+
return { isValid: false, error: `Invalid number: ${value}`, values: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const limits = CronParser.getFieldLimits(fieldName);
|
|
98
|
+
if (numValue < limits.min || numValue > limits.max) {
|
|
99
|
+
return { isValid: false, error: `Value ${numValue} out of range (${limits.min}-${limits.max})`, values: [] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { isValid: true, values: [numValue] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse step values like "*\/5" or "10/2"
|
|
107
|
+
*/
|
|
108
|
+
private static parseStepValue(fieldName: string, value: string): { isValid: boolean; error?: string; values: number[] } {
|
|
109
|
+
const parts = value.split('/');
|
|
110
|
+
if (parts.length !== 2) {
|
|
111
|
+
return { isValid: false, error: 'Invalid step format', values: [] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const [base, step] = parts;
|
|
115
|
+
if (!base || !step) {
|
|
116
|
+
return { isValid: false, error: 'Invalid step format', values: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stepNum = parseInt(step, 10);
|
|
120
|
+
|
|
121
|
+
if (isNaN(stepNum) || stepNum <= 0) {
|
|
122
|
+
return { isValid: false, error: 'Invalid step value', values: [] };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let allValues: number[];
|
|
126
|
+
if (base === '*') {
|
|
127
|
+
allValues = CronParser.getAllValuesForField(fieldName);
|
|
128
|
+
} else if (base.includes('-')) {
|
|
129
|
+
const rangeResult = CronParser.parseRangeValue(fieldName, base);
|
|
130
|
+
if (!rangeResult.isValid) {
|
|
131
|
+
return rangeResult;
|
|
132
|
+
}
|
|
133
|
+
allValues = rangeResult.values;
|
|
134
|
+
} else {
|
|
135
|
+
const baseNum = parseInt(base, 10);
|
|
136
|
+
if (isNaN(baseNum)) {
|
|
137
|
+
return { isValid: false, error: 'Invalid base value', values: [] };
|
|
138
|
+
}
|
|
139
|
+
const limits = CronParser.getFieldLimits(fieldName);
|
|
140
|
+
if (baseNum < limits.min || baseNum > limits.max) {
|
|
141
|
+
return { isValid: false, error: `Base value ${baseNum} out of range`, values: [] };
|
|
142
|
+
}
|
|
143
|
+
allValues = [baseNum];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const stepValues: number[] = [];
|
|
147
|
+
for (let i = 0; i < allValues.length; i += stepNum) {
|
|
148
|
+
const value = allValues[i];
|
|
149
|
+
if (value !== undefined) {
|
|
150
|
+
stepValues.push(value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { isValid: true, values: stepValues };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse range values like "1-5"
|
|
159
|
+
*/
|
|
160
|
+
private static parseRangeValue(fieldName: string, value: string): { isValid: boolean; error?: string; values: number[] } {
|
|
161
|
+
const parts = value.split('-');
|
|
162
|
+
if (parts.length !== 2) {
|
|
163
|
+
return { isValid: false, error: 'Invalid range format', values: [] };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const startStr = parts[0];
|
|
167
|
+
const endStr = parts[1];
|
|
168
|
+
if (!startStr || !endStr) {
|
|
169
|
+
return { isValid: false, error: 'Invalid range format', values: [] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const start = parseInt(startStr, 10);
|
|
173
|
+
const end = parseInt(endStr, 10);
|
|
174
|
+
|
|
175
|
+
if (isNaN(start) || isNaN(end)) {
|
|
176
|
+
return { isValid: false, error: 'Invalid range values', values: [] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const limits = CronParser.getFieldLimits(fieldName);
|
|
180
|
+
if (start < limits.min || start > limits.max || end < limits.min || end > limits.max) {
|
|
181
|
+
return { isValid: false, error: `Range values out of bounds (${limits.min}-${limits.max})`, values: [] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (start > end) {
|
|
185
|
+
return { isValid: false, error: 'Start value cannot be greater than end value', values: [] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const values: number[] = [];
|
|
189
|
+
for (let i = start; i <= end; i++) {
|
|
190
|
+
values.push(i);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { isValid: true, values };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parse list values like "1,3,5"
|
|
198
|
+
*/
|
|
199
|
+
private static parseListValue(fieldName: string, value: string): { isValid: boolean; error?: string; values: number[] } {
|
|
200
|
+
const parts = value.split(',');
|
|
201
|
+
const values: number[] = [];
|
|
202
|
+
const limits = CronParser.getFieldLimits(fieldName);
|
|
203
|
+
|
|
204
|
+
for (const part of parts) {
|
|
205
|
+
const numValue = parseInt(part.trim(), 10);
|
|
206
|
+
if (isNaN(numValue)) {
|
|
207
|
+
return { isValid: false, error: `Invalid list value: ${part}`, values: [] };
|
|
208
|
+
}
|
|
209
|
+
if (numValue < limits.min || numValue > limits.max) {
|
|
210
|
+
return { isValid: false, error: `List value ${numValue} out of range (${limits.min}-${limits.max})`, values: [] };
|
|
211
|
+
}
|
|
212
|
+
if (!values.includes(numValue)) {
|
|
213
|
+
values.push(numValue);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { isValid: true, values: values.sort((a, b) => a - b) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get all possible values for a field
|
|
222
|
+
*/
|
|
223
|
+
private static getAllValuesForField(fieldName: string): number[] {
|
|
224
|
+
const limits = this.getFieldLimits(fieldName);
|
|
225
|
+
const values: number[] = [];
|
|
226
|
+
for (let i = limits.min; i <= limits.max; i++) {
|
|
227
|
+
values.push(i);
|
|
228
|
+
}
|
|
229
|
+
return values;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get min/max limits for each field type
|
|
234
|
+
*/
|
|
235
|
+
private static getFieldLimits(fieldName: string): { min: number; max: number } {
|
|
236
|
+
switch (fieldName) {
|
|
237
|
+
case 'second':
|
|
238
|
+
case 'minute':
|
|
239
|
+
return { min: 0, max: 59 };
|
|
240
|
+
case 'hour':
|
|
241
|
+
return { min: 0, max: 23 };
|
|
242
|
+
case 'dayOfMonth':
|
|
243
|
+
return { min: 1, max: 31 };
|
|
244
|
+
case 'month':
|
|
245
|
+
return { min: 1, max: 12 };
|
|
246
|
+
case 'dayOfWeek':
|
|
247
|
+
return { min: 0, max: 7 }; // 0 and 7 both represent Sunday
|
|
248
|
+
default:
|
|
249
|
+
return { min: 0, max: 59 };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Calculate next execution time for a cron expression
|
|
255
|
+
* @param cronFields Parsed cron fields
|
|
256
|
+
* @param fromDate Date to calculate from (defaults to now)
|
|
257
|
+
* @returns Next execution date or null if no future execution
|
|
258
|
+
*/
|
|
259
|
+
static getNextExecution(cronFields: CronFields, fromDate: Date = new Date()): Date | null {
|
|
260
|
+
const date = new Date(fromDate.getTime());
|
|
261
|
+
|
|
262
|
+
// Start from the next minute to avoid returning the current time if it matches
|
|
263
|
+
date.setMinutes(date.getMinutes() + 1);
|
|
264
|
+
date.setSeconds(0, 0);
|
|
265
|
+
|
|
266
|
+
// Try up to 1 year in the future
|
|
267
|
+
const maxAttempts = 60 * 24 * 365; // 1 year in minutes
|
|
268
|
+
let attempts = 0;
|
|
269
|
+
|
|
270
|
+
while (attempts < maxAttempts) {
|
|
271
|
+
if (this.matchesCronFields(date, cronFields)) {
|
|
272
|
+
return new Date(date);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Move to next minute
|
|
276
|
+
date.setMinutes(date.getMinutes() + 1);
|
|
277
|
+
attempts++;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null; // No execution found within 1 year
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if a date matches the cron fields
|
|
285
|
+
*/
|
|
286
|
+
private static matchesCronFields(date: Date, fields: CronFields): boolean {
|
|
287
|
+
const minute = date.getMinutes();
|
|
288
|
+
const hour = date.getHours();
|
|
289
|
+
const dayOfMonth = date.getDate();
|
|
290
|
+
const month = date.getMonth() + 1; // JavaScript months are 0-based
|
|
291
|
+
const dayOfWeek = date.getDay(); // 0 = Sunday
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
(fields.second ? fields.second.includes(date.getSeconds()) : true) &&
|
|
295
|
+
fields.minute.includes(minute) &&
|
|
296
|
+
fields.hour.includes(hour) &&
|
|
297
|
+
fields.dayOfMonth.includes(dayOfMonth) &&
|
|
298
|
+
fields.month.includes(month) &&
|
|
299
|
+
(fields.dayOfWeek.includes(dayOfWeek) || fields.dayOfWeek.includes(dayOfWeek === 0 ? 7 : dayOfWeek))
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Validate a cron expression
|
|
305
|
+
* @param expression Cron expression to validate
|
|
306
|
+
* @returns Validation result
|
|
307
|
+
*/
|
|
308
|
+
static validate(expression: string): CronValidationResult {
|
|
309
|
+
return this.parse(expression);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get human-readable description of a cron expression
|
|
314
|
+
* @param expression Cron expression
|
|
315
|
+
* @returns Human-readable description
|
|
316
|
+
*/
|
|
317
|
+
static describe(expression: string): string {
|
|
318
|
+
const result = this.parse(expression);
|
|
319
|
+
if (!result.isValid || !result.fields) {
|
|
320
|
+
return 'Invalid cron expression';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const fields = result.fields;
|
|
324
|
+
const descriptions: string[] = [];
|
|
325
|
+
|
|
326
|
+
// Describe each field
|
|
327
|
+
if (fields.second && fields.second.length > 0) {
|
|
328
|
+
descriptions.push(`at second ${this.describeValues(fields.second)}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
descriptions.push(`at minute ${this.describeValues(fields.minute)}`);
|
|
332
|
+
descriptions.push(`at hour ${this.describeValues(fields.hour)}`);
|
|
333
|
+
descriptions.push(`on day ${this.describeValues(fields.dayOfMonth)} of the month`);
|
|
334
|
+
descriptions.push(`in month ${this.describeValues(fields.month)}`);
|
|
335
|
+
descriptions.push(`on day ${this.describeValues(fields.dayOfWeek)} of the week`);
|
|
336
|
+
|
|
337
|
+
return descriptions.join(', ');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get human-readable description of numeric values
|
|
342
|
+
*/
|
|
343
|
+
private static describeValues(values: number[]): string {
|
|
344
|
+
if (!values || values.length === 0) return 'none';
|
|
345
|
+
if (values.length === 1) return values[0]!.toString();
|
|
346
|
+
|
|
347
|
+
// Check for consecutive ranges
|
|
348
|
+
const ranges: string[] = [];
|
|
349
|
+
let start = values[0]!;
|
|
350
|
+
let prev = values[0]!;
|
|
351
|
+
|
|
352
|
+
for (let i = 1; i < values.length; i++) {
|
|
353
|
+
const current = values[i]!;
|
|
354
|
+
if (current === prev + 1) {
|
|
355
|
+
prev = current;
|
|
356
|
+
} else {
|
|
357
|
+
ranges.push(start === prev ? start.toString() : `${start}-${prev}`);
|
|
358
|
+
start = current;
|
|
359
|
+
prev = current;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
ranges.push(start === prev ? start.toString() : `${start}-${prev}`);
|
|
363
|
+
|
|
364
|
+
return ranges.join(',');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-friendly error message mappings for the BunSane framework
|
|
3
|
+
* Maps technical error codes and validation issues to clear, actionable messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ErrorMessage {
|
|
7
|
+
userMessage: string;
|
|
8
|
+
suggestion?: string;
|
|
9
|
+
category: 'validation' | 'authentication' | 'authorization' | 'system' | 'network';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ERROR_MESSAGES: Record<string, ErrorMessage> = {
|
|
13
|
+
// Validation Errors
|
|
14
|
+
'INVALID_EMAIL': {
|
|
15
|
+
userMessage: 'Please enter a valid email address',
|
|
16
|
+
suggestion: 'Check that your email follows the format: name@example.com',
|
|
17
|
+
category: 'validation'
|
|
18
|
+
},
|
|
19
|
+
'REQUIRED_FIELD': {
|
|
20
|
+
userMessage: 'This field is required',
|
|
21
|
+
suggestion: 'Please fill in all required fields before submitting',
|
|
22
|
+
category: 'validation'
|
|
23
|
+
},
|
|
24
|
+
'TOO_SHORT': {
|
|
25
|
+
userMessage: 'This value is too short',
|
|
26
|
+
suggestion: 'Please enter a longer value that meets the minimum requirements',
|
|
27
|
+
category: 'validation'
|
|
28
|
+
},
|
|
29
|
+
'TOO_LONG': {
|
|
30
|
+
userMessage: 'This value is too long',
|
|
31
|
+
suggestion: 'Please shorten your input to meet the maximum length requirement',
|
|
32
|
+
category: 'validation'
|
|
33
|
+
},
|
|
34
|
+
'INVALID_FORMAT': {
|
|
35
|
+
userMessage: 'This value has an invalid format',
|
|
36
|
+
suggestion: 'Please check the expected format and try again',
|
|
37
|
+
category: 'validation'
|
|
38
|
+
},
|
|
39
|
+
'DUPLICATE_VALUE': {
|
|
40
|
+
userMessage: 'This value already exists',
|
|
41
|
+
suggestion: 'Please choose a different value or contact support if you believe this is an error',
|
|
42
|
+
category: 'validation'
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Authentication Errors
|
|
46
|
+
'INVALID_CREDENTIALS': {
|
|
47
|
+
userMessage: 'Invalid username or password',
|
|
48
|
+
suggestion: 'Please check your credentials and try again, or use the forgot password option',
|
|
49
|
+
category: 'authentication'
|
|
50
|
+
},
|
|
51
|
+
'ACCOUNT_LOCKED': {
|
|
52
|
+
userMessage: 'Your account has been temporarily locked',
|
|
53
|
+
suggestion: 'Please wait a few minutes before trying again, or contact support',
|
|
54
|
+
category: 'authentication'
|
|
55
|
+
},
|
|
56
|
+
'SESSION_EXPIRED': {
|
|
57
|
+
userMessage: 'Your session has expired',
|
|
58
|
+
suggestion: 'Please log in again to continue',
|
|
59
|
+
category: 'authentication'
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Authorization Errors
|
|
63
|
+
'INSUFFICIENT_PERMISSIONS': {
|
|
64
|
+
userMessage: 'You don\'t have permission to perform this action',
|
|
65
|
+
suggestion: 'Please contact your administrator if you believe this is an error',
|
|
66
|
+
category: 'authorization'
|
|
67
|
+
},
|
|
68
|
+
'ACCESS_DENIED': {
|
|
69
|
+
userMessage: 'Access denied',
|
|
70
|
+
suggestion: 'You may not have the required permissions for this resource',
|
|
71
|
+
category: 'authorization'
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// System Errors
|
|
75
|
+
'INTERNAL_ERROR': {
|
|
76
|
+
userMessage: 'Something went wrong on our end',
|
|
77
|
+
suggestion: 'Please try again in a few moments. If the problem persists, contact support',
|
|
78
|
+
category: 'system'
|
|
79
|
+
},
|
|
80
|
+
'SERVICE_UNAVAILABLE': {
|
|
81
|
+
userMessage: 'Service is temporarily unavailable',
|
|
82
|
+
suggestion: 'Please try again later',
|
|
83
|
+
category: 'system'
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Network Errors
|
|
87
|
+
'NETWORK_ERROR': {
|
|
88
|
+
userMessage: 'Unable to connect to the server',
|
|
89
|
+
suggestion: 'Please check your internet connection and try again',
|
|
90
|
+
category: 'network'
|
|
91
|
+
},
|
|
92
|
+
'TIMEOUT_ERROR': {
|
|
93
|
+
userMessage: 'Request timed out',
|
|
94
|
+
suggestion: 'Please try again. If the problem continues, the service may be experiencing high load',
|
|
95
|
+
category: 'network'
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a user-friendly error message by code
|
|
101
|
+
*/
|
|
102
|
+
export function getErrorMessage(code: string): ErrorMessage {
|
|
103
|
+
return ERROR_MESSAGES[code] || {
|
|
104
|
+
userMessage: 'An unexpected error occurred',
|
|
105
|
+
suggestion: 'Please try again or contact support if the problem persists',
|
|
106
|
+
category: 'system'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Map Zod validation error paths to user-friendly error codes
|
|
112
|
+
*/
|
|
113
|
+
export const ZOD_ERROR_MAPPINGS: Record<string, string> = {
|
|
114
|
+
'email': 'INVALID_EMAIL',
|
|
115
|
+
'password': 'TOO_SHORT', // Will be refined based on actual validation rules
|
|
116
|
+
'username': 'INVALID_FORMAT',
|
|
117
|
+
'name': 'REQUIRED_FIELD',
|
|
118
|
+
'title': 'REQUIRED_FIELD',
|
|
119
|
+
'content': 'REQUIRED_FIELD',
|
|
120
|
+
'description': 'TOO_LONG'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Convert Zod error path to user-friendly error code
|
|
125
|
+
*/
|
|
126
|
+
export function mapZodPathToErrorCode(path: string[]): string {
|
|
127
|
+
// If path is empty, we can't determine the field, return generic error
|
|
128
|
+
if (path.length === 0) {
|
|
129
|
+
return 'INVALID_FORMAT';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fieldName = path[0]?.toLowerCase();
|
|
133
|
+
if (!fieldName) return 'INVALID_FORMAT';
|
|
134
|
+
|
|
135
|
+
// Direct field mappings
|
|
136
|
+
switch (fieldName) {
|
|
137
|
+
case 'email':
|
|
138
|
+
return 'INVALID_EMAIL';
|
|
139
|
+
case 'password':
|
|
140
|
+
return 'TOO_SHORT';
|
|
141
|
+
case 'username':
|
|
142
|
+
return 'INVALID_FORMAT';
|
|
143
|
+
case 'name':
|
|
144
|
+
case 'title':
|
|
145
|
+
case 'content':
|
|
146
|
+
case 'description':
|
|
147
|
+
return 'REQUIRED_FIELD';
|
|
148
|
+
default:
|
|
149
|
+
return 'INVALID_FORMAT';
|
|
150
|
+
}
|
|
151
|
+
}
|
package/core/Events.ts
DELETED
|
File without changes
|