bunsane 0.1.0 → 0.1.1

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 (85) 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 +119 -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 +159 -12
  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 +453 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +65 -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 +1 -1
  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/examples/hooks/README.md +228 -0
  52. package/examples/hooks/audit-logger.ts +495 -0
  53. package/gql/Generator.ts +56 -34
  54. package/gql/decorators/Upload.ts +176 -0
  55. package/gql/helpers.ts +67 -0
  56. package/gql/index.ts +55 -31
  57. package/gql/types.ts +1 -1
  58. package/index.ts +79 -11
  59. package/package.json +5 -4
  60. package/rest/Generator.ts +3 -0
  61. package/rest/index.ts +22 -0
  62. package/service/Service.ts +1 -1
  63. package/service/ServiceRegistry.ts +10 -6
  64. package/service/index.ts +12 -1
  65. package/tests/bench/insert.bench.ts +59 -0
  66. package/tests/bench/relations.bench.ts +269 -0
  67. package/tests/bench/sorting.bench.ts +415 -0
  68. package/tests/component-hooks.test.ts +1409 -0
  69. package/tests/component.test.ts +205 -0
  70. package/tests/errorHandling.test.ts +155 -0
  71. package/tests/hooks.test.ts +666 -0
  72. package/tests/query-sorting.test.ts +101 -0
  73. package/tests/relations.test.ts +169 -0
  74. package/tests/scheduler.test.ts +724 -0
  75. package/tsconfig.json +35 -34
  76. package/types/graphql.types.ts +87 -0
  77. package/types/hooks.types.ts +141 -0
  78. package/types/scheduler.types.ts +165 -0
  79. package/types/upload.types.ts +184 -0
  80. package/upload/index.ts +140 -0
  81. package/utils/UploadHelper.ts +305 -0
  82. package/utils/cronParser.ts +366 -0
  83. package/utils/errorMessages.ts +151 -0
  84. package/validate-docs.sh +90 -0
  85. package/core/Events.ts +0 -0
@@ -21,10 +21,20 @@ class EntityManager {
21
21
  this.entityQueue.push(entity);
22
22
  return resolve(true);
23
23
  } else {
24
- resolve(entity.doSave());
24
+ const result = await entity.doSave();
25
+ resolve(result);
26
+ }
27
+ })
28
+ }
29
+
30
+ public deleteEntity(entity: Entity, force: boolean = false) {
31
+ return new Promise<boolean>(async resolve => {
32
+ if(!this.dbReady) {
33
+ return resolve(false);
34
+ } else {
35
+ resolve(entity.doDelete(force));
25
36
  }
26
37
  })
27
-
28
38
  }
29
39
 
30
40
  private async savePendingEntities() {
@@ -1,5 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { GraphQLError, type GraphQLErrorOptions } from "graphql";
3
+ import { logger } from "./Logger";
4
+ import { getErrorMessage, mapZodPathToErrorCode, type ErrorMessage } from "../utils/errorMessages";
3
5
 
4
6
  export function responseError(message: string, extensions?: GraphQLErrorOptions) {
5
7
  return new GraphQLError(message, {
@@ -10,25 +12,80 @@ export function responseError(message: string, extensions?: GraphQLErrorOptions)
10
12
  });
11
13
  }
12
14
 
15
+ /**
16
+ * Create a user-friendly error response
17
+ */
18
+ export function createUserFriendlyError(code: string, customMessage?: string, extensions?: GraphQLErrorOptions) {
19
+ const errorInfo = getErrorMessage(code);
20
+
21
+ const baseExtensions = {
22
+ code,
23
+ category: errorInfo.category,
24
+ suggestion: errorInfo.suggestion,
25
+ userFriendly: true
26
+ };
27
+
28
+ return new GraphQLError(customMessage || errorInfo.userMessage, {
29
+ extensions: {
30
+ ...baseExtensions,
31
+ ...extensions?.extensions
32
+ }
33
+ });
34
+ }
35
+
13
36
  export function handleGraphQLError(err: any): never {
14
37
  if (err instanceof z.ZodError) {
15
- const errorMessages = err.issues.map((error: any) =>
16
- `${error.path.join('.')}: ${error.message}`
17
- ).join(', ');
18
-
19
- throw new GraphQLError(`Validation failed: ${errorMessages}`, {
38
+ // Convert Zod errors to user-friendly messages
39
+ const userFriendlyErrors = err.issues.map((issue: any) => {
40
+ const errorCode = mapZodPathToErrorCode(issue.path);
41
+ const errorInfo = getErrorMessage(errorCode);
42
+ return {
43
+ field: issue.path.join('.'),
44
+ message: errorInfo.userMessage,
45
+ suggestion: errorInfo.suggestion,
46
+ code: errorCode
47
+ };
48
+ });
49
+
50
+ if (userFriendlyErrors.length === 0) {
51
+ throw new GraphQLError("Validation failed", {
52
+ extensions: {
53
+ code: "VALIDATION_ERROR",
54
+ category: "validation",
55
+ userFriendly: true
56
+ }
57
+ });
58
+ }
59
+
60
+ const primaryError = userFriendlyErrors[0]!;
61
+ const errorMessage = userFriendlyErrors.length === 1
62
+ ? primaryError.message
63
+ : `${primaryError.message} (and ${userFriendlyErrors.length - 1} other validation issue${userFriendlyErrors.length > 2 ? 's' : ''})`;
64
+
65
+ throw new GraphQLError(errorMessage, {
20
66
  extensions: {
21
67
  code: "VALIDATION_ERROR",
22
- validationErrors: err.issues
68
+ category: "validation",
69
+ validationErrors: userFriendlyErrors,
70
+ suggestion: primaryError.suggestion,
71
+ userFriendly: true
23
72
  }
24
73
  });
25
74
  }
26
75
  if (err instanceof GraphQLError) {
27
76
  throw err;
28
77
  }
29
- throw new GraphQLError("An unexpected error occurred", {
78
+ logger.error("Unknown error in handleGraphQLError:");
79
+ logger.error(err);
80
+
81
+ const errorInfo = getErrorMessage("INTERNAL_ERROR");
82
+ throw new GraphQLError(errorInfo.userMessage, {
30
83
  extensions: {
31
84
  code: "INTERNAL_ERROR",
85
+ category: errorInfo.category,
86
+ suggestion: errorInfo.suggestion,
87
+ userFriendly: true,
88
+ node_env: process.env.NODE_ENV,
32
89
  originalError: process.env.NODE_ENV === 'development' ? err : undefined
33
90
  }
34
91
  });
@@ -0,0 +1,284 @@
1
+ import type { UploadConfiguration, ValidationResult } from "../types/upload.types";
2
+ import { logger as MainLogger } from "./Logger";
3
+
4
+ const logger = MainLogger.child({ scope: "FileValidator" });
5
+
6
+ /**
7
+ * File Validator - Comprehensive file validation and security checking
8
+ */
9
+ export class FileValidator {
10
+ private fileSignatures: Map<string, Uint8Array[]>;
11
+
12
+ constructor() {
13
+ this.fileSignatures = this.initializeFileSignatures();
14
+ }
15
+
16
+ /**
17
+ * Validate a file against configuration rules
18
+ */
19
+ public async validate(file: File, config: UploadConfiguration): Promise<ValidationResult> {
20
+ const errors: string[] = [];
21
+ const warnings: string[] = [];
22
+
23
+ logger.trace(`Validating file: ${file.name} (${file.size} bytes, ${file.type})`);
24
+
25
+ // File size validation
26
+ if (file.size > config.maxFileSize) {
27
+ errors.push(`File size ${file.size} exceeds maximum allowed size ${config.maxFileSize}`);
28
+ }
29
+
30
+ if (file.size === 0) {
31
+ errors.push("File is empty");
32
+ }
33
+
34
+ // MIME type validation
35
+ if (config.allowedMimeTypes.length > 0) {
36
+ if (!config.allowedMimeTypes.includes(file.type)) {
37
+ errors.push(`MIME type "${file.type}" is not allowed. Allowed types: ${config.allowedMimeTypes.join(", ")}`);
38
+ }
39
+ }
40
+
41
+ // File extension validation
42
+ if (config.allowedExtensions.length > 0) {
43
+ const extension = this.getFileExtension(file.name);
44
+ if (!config.allowedExtensions.includes(extension)) {
45
+ errors.push(`File extension "${extension}" is not allowed. Allowed extensions: ${config.allowedExtensions.join(", ")}`);
46
+ }
47
+ }
48
+
49
+ // File signature validation (magic numbers)
50
+ if (config.validateFileSignature) {
51
+ try {
52
+ const signatureValid = await this.validateFileSignature(file);
53
+ if (!signatureValid) {
54
+ errors.push("File signature does not match MIME type (possible file type spoofing)");
55
+ }
56
+ } catch (error) {
57
+ warnings.push(`Could not validate file signature: ${error instanceof Error ? error.message : 'Unknown error'}`);
58
+ }
59
+ }
60
+
61
+ // File name validation
62
+ const nameValidation = this.validateFileName(file.name);
63
+ if (!nameValidation.valid) {
64
+ errors.push(...nameValidation.errors);
65
+ }
66
+
67
+ // Custom validation
68
+ if (config.validation?.customValidators) {
69
+ for (const validator of config.validation.customValidators) {
70
+ try {
71
+ const result = await validator(file);
72
+ if (!result.valid) {
73
+ errors.push(...result.errors);
74
+ }
75
+ if (result.warnings) {
76
+ warnings.push(...result.warnings);
77
+ }
78
+ } catch (error) {
79
+ warnings.push(`Custom validator failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
80
+ }
81
+ }
82
+ }
83
+
84
+ const valid = errors.length === 0;
85
+
86
+ if (!valid) {
87
+ logger.warn(`File validation failed for ${file.name}: ${errors.join(', ')}`);
88
+ } else {
89
+ logger.trace(`File validation passed for ${file.name}`);
90
+ }
91
+
92
+ return {
93
+ valid,
94
+ errors,
95
+ warnings: warnings.length > 0 ? warnings : undefined
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Validate file name for security issues
101
+ */
102
+ public validateFileName(fileName: string): ValidationResult {
103
+ const errors: string[] = [];
104
+
105
+ // Check for path traversal attempts
106
+ if (fileName.includes("..") || fileName.includes("/") || fileName.includes("\\")) {
107
+ errors.push("File name contains invalid path characters");
108
+ }
109
+
110
+ // Check for dangerous characters
111
+ const dangerousChars = /[<>:"|?*\x00-\x1f]/;
112
+ if (dangerousChars.test(fileName)) {
113
+ errors.push("File name contains dangerous characters");
114
+ }
115
+
116
+ // Check for reserved names (Windows)
117
+ const reservedNames = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i;
118
+ if (reservedNames.test(fileName)) {
119
+ errors.push("File name is a reserved system name");
120
+ }
121
+
122
+ // Check for hidden files
123
+ if (fileName.startsWith(".")) {
124
+ errors.push("Hidden files are not allowed");
125
+ }
126
+
127
+ // Check file name length
128
+ if (fileName.length > 255) {
129
+ errors.push("File name is too long (maximum 255 characters)");
130
+ }
131
+
132
+ if (fileName.length === 0) {
133
+ errors.push("File name cannot be empty");
134
+ }
135
+
136
+ return {
137
+ valid: errors.length === 0,
138
+ errors
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Validate file signature against MIME type
144
+ */
145
+ private async validateFileSignature(file: File): Promise<boolean> {
146
+ const buffer = await file.slice(0, 32).arrayBuffer();
147
+ const bytes = new Uint8Array(buffer);
148
+
149
+ const expectedSignatures = this.getSignaturesForMimeType(file.type);
150
+ if (expectedSignatures.length === 0) {
151
+ // No known signatures for this MIME type
152
+ return true;
153
+ }
154
+
155
+ return expectedSignatures.some(signature =>
156
+ this.bytesMatch(bytes, signature)
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Check if bytes match a signature
162
+ */
163
+ private bytesMatch(bytes: Uint8Array, signature: Uint8Array): boolean {
164
+ if (bytes.length < signature.length) {
165
+ return false;
166
+ }
167
+
168
+ for (let i = 0; i < signature.length; i++) {
169
+ if (bytes[i] !== signature[i]) {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ return true;
175
+ }
176
+
177
+ /**
178
+ * Get file signatures for MIME type
179
+ */
180
+ private getSignaturesForMimeType(mimeType: string): Uint8Array[] {
181
+ return this.fileSignatures.get(mimeType) || [];
182
+ }
183
+
184
+ /**
185
+ * Extract file extension from filename
186
+ */
187
+ private getFileExtension(fileName: string): string {
188
+ const lastDot = fileName.lastIndexOf('.');
189
+ return lastDot > 0 ? fileName.slice(lastDot).toLowerCase() : '';
190
+ }
191
+
192
+ /**
193
+ * Initialize known file signatures (magic numbers)
194
+ */
195
+ private initializeFileSignatures(): Map<string, Uint8Array[]> {
196
+ const signatures = new Map<string, Uint8Array[]>();
197
+
198
+ // Image formats
199
+ signatures.set("image/jpeg", [
200
+ new Uint8Array([0xFF, 0xD8, 0xFF])
201
+ ]);
202
+
203
+ signatures.set("image/png", [
204
+ new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
205
+ ]);
206
+
207
+ signatures.set("image/gif", [
208
+ new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]), // GIF87a
209
+ new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]) // GIF89a
210
+ ]);
211
+
212
+ signatures.set("image/webp", [
213
+ new Uint8Array([0x52, 0x49, 0x46, 0x46]) // RIFF (WebP is RIFF-based)
214
+ ]);
215
+
216
+ // Document formats
217
+ signatures.set("application/pdf", [
218
+ new Uint8Array([0x25, 0x50, 0x44, 0x46]) // %PDF
219
+ ]);
220
+
221
+ // Archive formats
222
+ signatures.set("application/zip", [
223
+ new Uint8Array([0x50, 0x4B, 0x03, 0x04]), // ZIP
224
+ new Uint8Array([0x50, 0x4B, 0x05, 0x06]), // Empty ZIP
225
+ new Uint8Array([0x50, 0x4B, 0x07, 0x08]) // Spanned ZIP
226
+ ]);
227
+
228
+ return signatures;
229
+ }
230
+
231
+ /**
232
+ * Get human-readable validation summary
233
+ */
234
+ public getValidationSummary(result: ValidationResult): string {
235
+ if (result.valid) {
236
+ return "File validation passed";
237
+ }
238
+
239
+ let summary = `File validation failed: ${result.errors.join("; ")}`;
240
+
241
+ if (result.warnings && result.warnings.length > 0) {
242
+ summary += ` | Warnings: ${result.warnings.join("; ")}`;
243
+ }
244
+
245
+ return summary;
246
+ }
247
+
248
+ /**
249
+ * Check if file is potentially dangerous
250
+ */
251
+ public async isDangerous(file: File): Promise<boolean> {
252
+ // Check for executable file extensions
253
+ const dangerousExtensions = [
254
+ '.exe', '.scr', '.bat', '.cmd', '.com', '.pif', '.vbs', '.js', '.jar',
255
+ '.sh', '.py', '.pl', '.php', '.asp', '.aspx', '.jsp'
256
+ ];
257
+
258
+ const extension = this.getFileExtension(file.name);
259
+ if (dangerousExtensions.includes(extension)) {
260
+ return true;
261
+ }
262
+
263
+ // Check for polyglot files (files that are valid in multiple formats)
264
+ try {
265
+ const buffer = await file.slice(0, 1024).arrayBuffer();
266
+ const bytes = new Uint8Array(buffer);
267
+ const content = new TextDecoder().decode(bytes);
268
+
269
+ // Look for script patterns
270
+ const scriptPatterns = [
271
+ /<script/i,
272
+ /javascript:/i,
273
+ /vbscript:/i,
274
+ /<iframe/i,
275
+ /<object/i,
276
+ /<embed/i
277
+ ];
278
+
279
+ return scriptPatterns.some(pattern => pattern.test(content));
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+ }