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,338 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { BaseComponent, CompData, Component } from "../core/Components";
|
|
3
|
+
import type { ComponentDataType } from "../core/Components";
|
|
4
|
+
import { Entity } from "../core/Entity";
|
|
5
|
+
import App from "../core/App";
|
|
6
|
+
import ComponentRegistry from "../core/ComponentRegistry";
|
|
7
|
+
import db from "../database";
|
|
8
|
+
|
|
9
|
+
// Test component with 'value' attribute (standard assumption)
|
|
10
|
+
@Component
|
|
11
|
+
class ValueComponent extends BaseComponent {
|
|
12
|
+
@CompData()
|
|
13
|
+
value: string = "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Test component with different attribute name
|
|
17
|
+
@Component
|
|
18
|
+
class CustomAttributeComponent extends BaseComponent {
|
|
19
|
+
@CompData()
|
|
20
|
+
customData: string = "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Test component with multiple attributes
|
|
24
|
+
@Component
|
|
25
|
+
class MultiAttributeComponent extends BaseComponent {
|
|
26
|
+
@CompData()
|
|
27
|
+
title: string = "";
|
|
28
|
+
@CompData()
|
|
29
|
+
content: string = "";
|
|
30
|
+
@CompData({ indexed: true })
|
|
31
|
+
category: string = "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Test component with no data attributes
|
|
35
|
+
@Component
|
|
36
|
+
class NoDataComponent extends BaseComponent {
|
|
37
|
+
// No @CompData properties
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Test component with numeric value
|
|
41
|
+
@Component
|
|
42
|
+
class NumericComponent extends BaseComponent {
|
|
43
|
+
@CompData()
|
|
44
|
+
count: number = 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Test component with boolean value
|
|
48
|
+
@Component
|
|
49
|
+
class BooleanComponent extends BaseComponent {
|
|
50
|
+
@CompData()
|
|
51
|
+
enabled: boolean = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let app: App;
|
|
55
|
+
|
|
56
|
+
beforeAll(async () => {
|
|
57
|
+
app = new App();
|
|
58
|
+
await app.waitForAppReady();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("Component Edge Cases and Attribute Handling", () => {
|
|
62
|
+
describe("Component with standard 'value' attribute", () => {
|
|
63
|
+
test("should correctly identify and return value property", () => {
|
|
64
|
+
const comp = new ValueComponent();
|
|
65
|
+
comp.value = "test value";
|
|
66
|
+
|
|
67
|
+
const props = comp.properties();
|
|
68
|
+
expect(props).toContain("value");
|
|
69
|
+
expect(props).toHaveLength(1);
|
|
70
|
+
|
|
71
|
+
const data = comp.data();
|
|
72
|
+
expect(data.value).toBe("test value");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("Component with custom attribute name", () => {
|
|
77
|
+
test("should correctly identify and return custom property", () => {
|
|
78
|
+
const comp = new CustomAttributeComponent();
|
|
79
|
+
comp.customData = "custom data";
|
|
80
|
+
|
|
81
|
+
const props = comp.properties();
|
|
82
|
+
expect(props).toContain("customData");
|
|
83
|
+
expect(props).toHaveLength(1);
|
|
84
|
+
|
|
85
|
+
const data = comp.data();
|
|
86
|
+
expect(data.customData).toBe("custom data");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("Component with multiple attributes", () => {
|
|
91
|
+
test("should correctly identify all data properties", () => {
|
|
92
|
+
const comp = new MultiAttributeComponent();
|
|
93
|
+
comp.title = "Test Title";
|
|
94
|
+
comp.content = "Test Content";
|
|
95
|
+
comp.category = "Test Category";
|
|
96
|
+
|
|
97
|
+
const props = comp.properties();
|
|
98
|
+
expect(props).toContain("title");
|
|
99
|
+
expect(props).toContain("content");
|
|
100
|
+
expect(props).toContain("category");
|
|
101
|
+
expect(props).toHaveLength(3);
|
|
102
|
+
|
|
103
|
+
const indexedProps = comp.indexedProperties();
|
|
104
|
+
expect(indexedProps).toContain("category");
|
|
105
|
+
expect(indexedProps).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("should return all data in data() method", () => {
|
|
109
|
+
const comp = new MultiAttributeComponent();
|
|
110
|
+
comp.title = "Title";
|
|
111
|
+
comp.content = "Content";
|
|
112
|
+
comp.category = "Category";
|
|
113
|
+
|
|
114
|
+
const data = comp.data();
|
|
115
|
+
expect(data.title).toBe("Title");
|
|
116
|
+
expect(data.content).toBe("Content");
|
|
117
|
+
expect(data.category).toBe("Category");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("Component with no data attributes", () => {
|
|
122
|
+
test("should have empty properties array", () => {
|
|
123
|
+
const comp = new NoDataComponent();
|
|
124
|
+
|
|
125
|
+
const props = comp.properties();
|
|
126
|
+
expect(props).toHaveLength(0);
|
|
127
|
+
|
|
128
|
+
const data = comp.data();
|
|
129
|
+
expect(Object.keys(data)).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Component with numeric attribute", () => {
|
|
134
|
+
test("should handle numeric values correctly", () => {
|
|
135
|
+
const comp = new NumericComponent();
|
|
136
|
+
comp.count = 42;
|
|
137
|
+
|
|
138
|
+
const data = comp.data();
|
|
139
|
+
expect(data.count).toBe(42);
|
|
140
|
+
expect(typeof data.count).toBe("number");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("Component with boolean attribute", () => {
|
|
145
|
+
test("should handle boolean values correctly", () => {
|
|
146
|
+
const comp = new BooleanComponent();
|
|
147
|
+
comp.enabled = true;
|
|
148
|
+
|
|
149
|
+
const data = comp.data();
|
|
150
|
+
expect(data.enabled).toBe(true);
|
|
151
|
+
expect(typeof data.enabled).toBe("boolean");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("Component update operations", () => {
|
|
156
|
+
test("should handle partial updates for multi-attribute components", () => {
|
|
157
|
+
const comp = new MultiAttributeComponent();
|
|
158
|
+
comp.title = "Initial Title";
|
|
159
|
+
comp.content = "Initial Content";
|
|
160
|
+
comp.category = "Initial Category";
|
|
161
|
+
|
|
162
|
+
// Simulate partial update
|
|
163
|
+
comp.title = "Updated Title";
|
|
164
|
+
|
|
165
|
+
const data = comp.data();
|
|
166
|
+
expect(data.title).toBe("Updated Title");
|
|
167
|
+
expect(data.content).toBe("Initial Content");
|
|
168
|
+
expect(data.category).toBe("Initial Category");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("Component type safety and data integrity", () => {
|
|
173
|
+
test("should maintain type safety for ComponentDataType", () => {
|
|
174
|
+
const comp = new ValueComponent();
|
|
175
|
+
comp.value = "test";
|
|
176
|
+
|
|
177
|
+
const data: ComponentDataType<ValueComponent> = comp.data();
|
|
178
|
+
expect(data.value).toBe("test");
|
|
179
|
+
// TypeScript should prevent accessing non-existent properties
|
|
180
|
+
// This test ensures the type system works correctly
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("should exclude non-data properties from data()", () => {
|
|
184
|
+
const comp = new ValueComponent();
|
|
185
|
+
comp.value = "test";
|
|
186
|
+
// id is not a data property
|
|
187
|
+
comp.id = "some-id";
|
|
188
|
+
|
|
189
|
+
const data = comp.data();
|
|
190
|
+
expect(data.value).toBe("test");
|
|
191
|
+
expect((data as any).id).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("Component registry and type identification", () => {
|
|
196
|
+
test("should generate unique type IDs for different components", () => {
|
|
197
|
+
const comp1 = new ValueComponent();
|
|
198
|
+
const comp2 = new CustomAttributeComponent();
|
|
199
|
+
|
|
200
|
+
expect(comp1.getTypeID()).not.toBe(comp2.getTypeID());
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("Error handling for malformed components", () => {
|
|
205
|
+
test("should handle components with undefined values gracefully", () => {
|
|
206
|
+
const comp = new ValueComponent();
|
|
207
|
+
// value is undefined initially
|
|
208
|
+
|
|
209
|
+
const data = comp.data();
|
|
210
|
+
expect(data.value).toBe(""); // Default value
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("Component Removal from Entities", () => {
|
|
216
|
+
test("should successfully remove an existing component from an entity", () => {
|
|
217
|
+
const entity = Entity.Create();
|
|
218
|
+
entity.add(ValueComponent, { value: "test" });
|
|
219
|
+
|
|
220
|
+
// Verify component is added
|
|
221
|
+
expect(entity.componentList()).toHaveLength(1);
|
|
222
|
+
expect(entity.componentList()[0]).toBeInstanceOf(ValueComponent);
|
|
223
|
+
|
|
224
|
+
// Remove the component
|
|
225
|
+
const removed = entity.remove(ValueComponent);
|
|
226
|
+
expect(removed).toBe(true);
|
|
227
|
+
|
|
228
|
+
// Verify component is removed
|
|
229
|
+
expect(entity.componentList()).toHaveLength(0);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("should return false when trying to remove a non-existent component", () => {
|
|
233
|
+
const entity = Entity.Create();
|
|
234
|
+
|
|
235
|
+
// Try to remove a component that was never added
|
|
236
|
+
const removed = entity.remove(ValueComponent);
|
|
237
|
+
expect(removed).toBe(false);
|
|
238
|
+
|
|
239
|
+
// Entity should still have no components
|
|
240
|
+
expect(entity.componentList()).toHaveLength(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("should remove only the specified component type, leaving others intact", () => {
|
|
244
|
+
const entity = Entity.Create();
|
|
245
|
+
entity.add(ValueComponent, { value: "value comp" });
|
|
246
|
+
entity.add(CustomAttributeComponent, { customData: "custom comp" });
|
|
247
|
+
|
|
248
|
+
// Verify both components are added
|
|
249
|
+
expect(entity.componentList()).toHaveLength(2);
|
|
250
|
+
|
|
251
|
+
// Remove only the ValueComponent
|
|
252
|
+
const removed = entity.remove(ValueComponent);
|
|
253
|
+
expect(removed).toBe(true);
|
|
254
|
+
|
|
255
|
+
// Verify only CustomAttributeComponent remains
|
|
256
|
+
expect(entity.componentList()).toHaveLength(1);
|
|
257
|
+
expect(entity.componentList()[0]).toBeInstanceOf(CustomAttributeComponent);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("should mark entity as dirty after component removal", () => {
|
|
261
|
+
const entity = Entity.Create();
|
|
262
|
+
entity.add(ValueComponent, { value: "test" });
|
|
263
|
+
|
|
264
|
+
// Entity should be dirty after adding
|
|
265
|
+
expect((entity as any)._dirty).toBe(true);
|
|
266
|
+
|
|
267
|
+
// Save to make it clean
|
|
268
|
+
// Note: We can't actually save without database, but we can set it manually for test
|
|
269
|
+
entity.setDirty(false);
|
|
270
|
+
expect((entity as any)._dirty).toBe(false);
|
|
271
|
+
|
|
272
|
+
// Remove component
|
|
273
|
+
entity.remove(ValueComponent);
|
|
274
|
+
|
|
275
|
+
// Entity should be dirty again
|
|
276
|
+
expect((entity as any)._dirty).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("should persist component addition and removal to database", async () => {
|
|
280
|
+
const entity = Entity.Create();
|
|
281
|
+
entity.add(ValueComponent, { value: "test value" });
|
|
282
|
+
|
|
283
|
+
// Save to database
|
|
284
|
+
const saveResult = await entity.save();
|
|
285
|
+
expect(saveResult).toBe(true);
|
|
286
|
+
|
|
287
|
+
// Verify component exists in database
|
|
288
|
+
const componentsAfterAdd = await db`SELECT id, data FROM components WHERE entity_id = ${entity.id} AND deleted_at IS NULL`;
|
|
289
|
+
expect(componentsAfterAdd.length).toBe(1);
|
|
290
|
+
expect(componentsAfterAdd[0].data.value).toBe("test value");
|
|
291
|
+
|
|
292
|
+
// Remove the component
|
|
293
|
+
const removed = entity.remove(ValueComponent);
|
|
294
|
+
expect(removed).toBe(true);
|
|
295
|
+
|
|
296
|
+
// Save again to persist removal
|
|
297
|
+
const saveResult2 = await entity.save();
|
|
298
|
+
expect(saveResult2).toBe(true);
|
|
299
|
+
|
|
300
|
+
// Verify component is removed from database
|
|
301
|
+
const componentsAfterRemove = await db`SELECT id, data FROM components WHERE entity_id = ${entity.id} AND deleted_at IS NULL`;
|
|
302
|
+
expect(componentsAfterRemove.length).toBe(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("should handle multiple component additions and removals in database", async () => {
|
|
306
|
+
const entity = Entity.Create();
|
|
307
|
+
entity.add(ValueComponent, { value: "value comp" });
|
|
308
|
+
entity.add(CustomAttributeComponent, { customData: "custom comp" });
|
|
309
|
+
|
|
310
|
+
// Save to database
|
|
311
|
+
await entity.save();
|
|
312
|
+
|
|
313
|
+
// Verify both components exist
|
|
314
|
+
const componentsAfterAdd = await db`SELECT type_id, data FROM components WHERE entity_id = ${entity.id} AND deleted_at IS NULL ORDER BY type_id`;
|
|
315
|
+
expect(componentsAfterAdd.length).toBe(2);
|
|
316
|
+
const valueComp = componentsAfterAdd.find((c: any) => c.data.value === "value comp");
|
|
317
|
+
const customComp = componentsAfterAdd.find((c: any) => c.data.customData === "custom comp");
|
|
318
|
+
expect(valueComp).toBeDefined();
|
|
319
|
+
expect(customComp).toBeDefined();
|
|
320
|
+
|
|
321
|
+
// Remove one component
|
|
322
|
+
entity.remove(ValueComponent);
|
|
323
|
+
await entity.save();
|
|
324
|
+
|
|
325
|
+
// Verify only custom component remains
|
|
326
|
+
const componentsAfterRemove = await db`SELECT type_id, data FROM components WHERE entity_id = ${entity.id} AND deleted_at IS NULL`;
|
|
327
|
+
expect(componentsAfterRemove.length).toBe(1);
|
|
328
|
+
expect(componentsAfterRemove[0].data.customData).toBe("custom comp");
|
|
329
|
+
|
|
330
|
+
// Remove the last component
|
|
331
|
+
entity.remove(CustomAttributeComponent);
|
|
332
|
+
await entity.save();
|
|
333
|
+
|
|
334
|
+
// Verify no components remain
|
|
335
|
+
const componentsAfterRemoveAll = await db`SELECT id FROM components WHERE entity_id = ${entity.id} AND deleted_at IS NULL`;
|
|
336
|
+
expect(componentsAfterRemoveAll.length).toBe(0);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createUserFriendlyError, handleGraphQLError } from "../core/ErrorHandler";
|
|
3
|
+
import { getErrorMessage, mapZodPathToErrorCode } from "../utils/errorMessages";
|
|
4
|
+
import { GraphQLError } from "graphql";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
|
|
7
|
+
describe('Error Handling Phase 1 Tests', () => {
|
|
8
|
+
describe('User-Friendly Error Messages', () => {
|
|
9
|
+
test('should return correct error message for known error code', () => {
|
|
10
|
+
const errorInfo = getErrorMessage('INVALID_EMAIL');
|
|
11
|
+
expect(errorInfo.userMessage).toBe('Please enter a valid email address');
|
|
12
|
+
expect(errorInfo.suggestion).toBe('Check that your email follows the format: name@example.com');
|
|
13
|
+
expect(errorInfo.category).toBe('validation');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should return fallback message for unknown error code', () => {
|
|
17
|
+
const errorInfo = getErrorMessage('UNKNOWN_CODE');
|
|
18
|
+
expect(errorInfo.userMessage).toBe('An unexpected error occurred');
|
|
19
|
+
expect(errorInfo.category).toBe('system');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should map Zod paths to error codes correctly', () => {
|
|
23
|
+
expect(mapZodPathToErrorCode(['email'])).toBe('INVALID_EMAIL');
|
|
24
|
+
expect(mapZodPathToErrorCode(['password'])).toBe('TOO_SHORT');
|
|
25
|
+
expect(mapZodPathToErrorCode(['unknownField'])).toBe('INVALID_FORMAT');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('createUserFriendlyError function', () => {
|
|
30
|
+
test('should create GraphQL error with user-friendly message', () => {
|
|
31
|
+
const error = createUserFriendlyError('INVALID_EMAIL');
|
|
32
|
+
|
|
33
|
+
expect(error).toBeInstanceOf(GraphQLError);
|
|
34
|
+
expect(error.message).toBe('Please enter a valid email address');
|
|
35
|
+
expect(error.extensions).toEqual({
|
|
36
|
+
code: 'INVALID_EMAIL',
|
|
37
|
+
category: 'validation',
|
|
38
|
+
suggestion: 'Check that your email follows the format: name@example.com',
|
|
39
|
+
userFriendly: true
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should allow custom message override', () => {
|
|
44
|
+
const customMessage = 'Custom email error';
|
|
45
|
+
const error = createUserFriendlyError('INVALID_EMAIL', customMessage);
|
|
46
|
+
|
|
47
|
+
expect(error.message).toBe(customMessage);
|
|
48
|
+
expect(error.extensions?.code).toBe('INVALID_EMAIL');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should merge additional extensions', () => {
|
|
52
|
+
const error = createUserFriendlyError('INVALID_EMAIL', undefined, {
|
|
53
|
+
extensions: { additionalField: 'test' }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(error.extensions?.additionalField).toBe('test');
|
|
57
|
+
expect(error.extensions?.userFriendly).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('handleGraphQLError function', () => {
|
|
62
|
+
test('should handle Zod validation errors with user-friendly messages', () => {
|
|
63
|
+
// Create a real Zod error by validating invalid data with a field name
|
|
64
|
+
const userSchema = z.object({
|
|
65
|
+
email: z.string().email()
|
|
66
|
+
});
|
|
67
|
+
let zodError: z.ZodError;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
userSchema.parse({ email: 'invalid-email' });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
zodError = error as z.ZodError;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
expect(() => handleGraphQLError(zodError!)).toThrow(GraphQLError);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
handleGraphQLError(zodError!);
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
expect(error.message).toBe('Please enter a valid email address');
|
|
81
|
+
expect(error.extensions?.code).toBe('VALIDATION_ERROR');
|
|
82
|
+
expect(error.extensions?.category).toBe('validation');
|
|
83
|
+
expect(error.extensions?.userFriendly).toBe(true);
|
|
84
|
+
expect(error.extensions?.validationErrors).toBeDefined();
|
|
85
|
+
expect(error.extensions?.suggestion).toBe('Check that your email follows the format: name@example.com');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should handle multiple Zod validation errors', () => {
|
|
90
|
+
// Create a schema that will produce multiple validation errors
|
|
91
|
+
const userSchema = z.object({
|
|
92
|
+
email: z.string().email(),
|
|
93
|
+
password: z.string().min(8)
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let zodError: z.ZodError;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
userSchema.parse({
|
|
100
|
+
email: 'invalid-email',
|
|
101
|
+
password: 'short'
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
zodError = error as z.ZodError;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(() => handleGraphQLError(zodError!)).toThrow(GraphQLError);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
handleGraphQLError(zodError!);
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
expect(error.message).toContain('Please enter a valid email address');
|
|
113
|
+
expect(error.extensions?.validationErrors).toHaveLength(2);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('should handle empty Zod errors gracefully', () => {
|
|
118
|
+
const zodError = new z.ZodError([]);
|
|
119
|
+
|
|
120
|
+
expect(() => handleGraphQLError(zodError)).toThrow(GraphQLError);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
handleGraphQLError(zodError);
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
expect(error.message).toBe('Validation failed');
|
|
126
|
+
expect(error.extensions?.code).toBe('VALIDATION_ERROR');
|
|
127
|
+
expect(error.extensions?.userFriendly).toBe(true);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should re-throw existing GraphQL errors', () => {
|
|
132
|
+
const originalError = new GraphQLError('Original error', {
|
|
133
|
+
extensions: { code: 'ORIGINAL' }
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(() => handleGraphQLError(originalError)).toThrow(originalError);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should handle unknown errors with user-friendly message', () => {
|
|
140
|
+
const unknownError = new Error('Some unknown error');
|
|
141
|
+
|
|
142
|
+
expect(() => handleGraphQLError(unknownError)).toThrow(GraphQLError);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
handleGraphQLError(unknownError);
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
expect(error.message).toBe('Something went wrong on our end');
|
|
148
|
+
expect(error.extensions?.code).toBe('INTERNAL_ERROR');
|
|
149
|
+
expect(error.extensions?.category).toBe('system');
|
|
150
|
+
expect(error.extensions?.suggestion).toBe('Please try again in a few moments. If the problem persists, contact support');
|
|
151
|
+
expect(error.extensions?.userFriendly).toBe(true);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|