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.
Files changed (82) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +168 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +193 -14
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +503 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +89 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +5 -5
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/gql/Generator.ts +58 -35
  52. package/gql/decorators/Upload.ts +176 -0
  53. package/gql/helpers.ts +67 -0
  54. package/gql/index.ts +65 -31
  55. package/gql/types.ts +1 -1
  56. package/index.ts +79 -11
  57. package/package.json +19 -10
  58. package/rest/Generator.ts +3 -0
  59. package/rest/index.ts +22 -0
  60. package/service/Service.ts +1 -1
  61. package/service/ServiceRegistry.ts +10 -6
  62. package/service/index.ts +12 -1
  63. package/tests/bench/insert.bench.ts +59 -0
  64. package/tests/bench/relations.bench.ts +269 -0
  65. package/tests/bench/sorting.bench.ts +415 -0
  66. package/tests/component-hooks.test.ts +1409 -0
  67. package/tests/component.test.ts +338 -0
  68. package/tests/errorHandling.test.ts +155 -0
  69. package/tests/hooks.test.ts +666 -0
  70. package/tests/query-sorting.test.ts +101 -0
  71. package/tests/relations.test.ts +169 -0
  72. package/tests/scheduler.test.ts +724 -0
  73. package/tsconfig.json +35 -34
  74. package/types/graphql.types.ts +87 -0
  75. package/types/hooks.types.ts +141 -0
  76. package/types/scheduler.types.ts +165 -0
  77. package/types/upload.types.ts +184 -0
  78. package/upload/index.ts +140 -0
  79. package/utils/UploadHelper.ts +305 -0
  80. package/utils/cronParser.ts +366 -0
  81. package/utils/errorMessages.ts +151 -0
  82. 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