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,101 @@
1
+ import { describe, it, expect, beforeAll, mock } from "bun:test";
2
+ import Query from "../core/Query";
3
+ import { BaseComponent, CompData, Component } from "../core/Components";
4
+ import ComponentRegistry from "../core/ComponentRegistry";
5
+
6
+ // Define test components
7
+ @Component
8
+ class TestUserComponent extends BaseComponent {
9
+ @CompData()
10
+ name: string = "";
11
+
12
+ @CompData()
13
+ age: number = 0;
14
+
15
+ @CompData()
16
+ score: number = 0;
17
+ }
18
+
19
+ @Component
20
+ class TestPostComponent extends BaseComponent {
21
+ @CompData()
22
+ title: string = "";
23
+
24
+ @CompData()
25
+ createdAt: string = "";
26
+ }
27
+
28
+ describe("Query Sorting", () => {
29
+ beforeAll(() => {
30
+ // Mock ComponentRegistry for testing
31
+ mock.restore();
32
+
33
+ // Mock the getComponentId method to return predictable IDs
34
+ (ComponentRegistry as any).getComponentId = mock((name: string) => {
35
+ if (name === "TestUserComponent") return "test-user-type-id";
36
+ if (name === "TestPostComponent") return "test-post-type-id";
37
+ return undefined;
38
+ });
39
+ });
40
+
41
+ it("should create a query with sortBy method", () => {
42
+ const query = new Query();
43
+ const result = query.with(TestUserComponent).sortBy(TestUserComponent, "age", "DESC");
44
+
45
+ expect(result).toBeInstanceOf(Query);
46
+ expect((result as any).sortOrders).toHaveLength(1);
47
+ expect((result as any).sortOrders[0]).toEqual({
48
+ component: "TestUserComponent",
49
+ property: "age",
50
+ direction: "DESC",
51
+ nullsFirst: false
52
+ });
53
+ });
54
+
55
+ it("should validate component is included in query before sorting", () => {
56
+ const query = new Query();
57
+
58
+ expect(() => {
59
+ query.sortBy(TestUserComponent, "age");
60
+ }).toThrow("Cannot sort by component TestUserComponent that is not included in the query");
61
+ });
62
+
63
+ it("should support orderBy with multiple sort orders", () => {
64
+ const query = new Query();
65
+ const sortOrders = [
66
+ { component: "TestUserComponent", property: "age", direction: "DESC" as const },
67
+ { component: "TestUserComponent", property: "name", direction: "ASC" as const }
68
+ ];
69
+
70
+ const result = query.with(TestUserComponent).orderBy(sortOrders);
71
+
72
+ expect((result as any).sortOrders).toEqual(sortOrders);
73
+ });
74
+
75
+ it("should build correct ORDER BY clause for sorting", () => {
76
+ const query = new Query();
77
+ query.with(TestUserComponent).sortBy(TestUserComponent, "age", "DESC");
78
+
79
+ const orderByClause = (query as any).buildOrderByClause(["test-user-type-id"]);
80
+
81
+ expect(orderByClause).toContain("ORDER BY");
82
+ expect(orderByClause).toContain("DESC");
83
+ expect(orderByClause).toContain("ec.entity_id ASC");
84
+ });
85
+
86
+ it("should handle nulls first option", () => {
87
+ const query = new Query();
88
+ query.with(TestUserComponent).sortBy(TestUserComponent, "age", "ASC", true);
89
+
90
+ expect((query as any).sortOrders[0].nullsFirst).toBe(true);
91
+ });
92
+
93
+ it("should build ORDER BY clause with nulls first", () => {
94
+ const query = new Query();
95
+ query.with(TestUserComponent).sortBy(TestUserComponent, "age", "ASC", true);
96
+
97
+ const orderByClause = (query as any).buildOrderByClause(["test-user-type-id"]);
98
+
99
+ expect(orderByClause).toContain("NULLS FIRST");
100
+ });
101
+ });
@@ -0,0 +1,169 @@
1
+ import { describe, test, expect, beforeAll, beforeEach } from "bun:test";
2
+ import App from "core/App";
3
+ import { BaseComponent, CompData, Component } from "core/Components";
4
+ import { Entity } from "core/Entity";
5
+ import { BatchLoader } from "core/BatchLoader";
6
+ import { isFieldRequestedSafe } from "gql/helpers";
7
+ import db from "database";
8
+
9
+ let app: App;
10
+
11
+ beforeAll(async () => {
12
+ app = new App();
13
+ await app.waitForAppReady();
14
+ });
15
+
16
+ beforeEach(async () => {
17
+ await db`TRUNCATE TABLE entities CASCADE;`;
18
+ });
19
+
20
+ @Component
21
+ class AuthorComponent extends BaseComponent {
22
+ @CompData()
23
+ value: string = ""; // related user id
24
+ }
25
+
26
+ @Component
27
+ class TitleComponent extends BaseComponent {
28
+ @CompData()
29
+ value: string = "";
30
+ }
31
+
32
+ @Component
33
+ class UserComponent extends BaseComponent {
34
+ @CompData()
35
+ name: string = "";
36
+ }
37
+
38
+ describe('Relations Tests', () => {
39
+ test('BatchLoader.loadRelatedEntitiesBatched fetches relations in one query', async () => {
40
+ // Create users
41
+ const user1 = Entity.Create().add(UserComponent, { name: "User1" });
42
+ const user2 = Entity.Create().add(UserComponent, { name: "User2" });
43
+ await Promise.all([user1.save(), user2.save()]);
44
+
45
+ // Create posts with authors
46
+ const post1 = Entity.Create()
47
+ .add(TitleComponent, { value: "Post1" })
48
+ .add(AuthorComponent, { value: user1.id });
49
+ const post2 = Entity.Create()
50
+ .add(TitleComponent, { value: "Post2" })
51
+ .add(AuthorComponent, { value: user2.id });
52
+ await Promise.all([post1.save(), post2.save()]);
53
+
54
+ // Test batched loading
55
+ const loader = async (ids: string[]) => {
56
+ const entities = await Entity.LoadMultiple(ids);
57
+ return entities;
58
+ };
59
+
60
+ const result = await BatchLoader.loadRelatedEntitiesBatched([post1, post2], AuthorComponent, loader);
61
+
62
+ expect(result.size).toBe(2);
63
+ expect(result.get(user1.id)?.id).toBe(user1.id);
64
+ expect(result.get(user2.id)?.id).toBe(user2.id);
65
+ });
66
+
67
+ test('BatchLoader handles large parent list efficiently', async () => {
68
+ // Create 100 users
69
+ const users = [];
70
+ for (let i = 0; i < 100; i++) {
71
+ const user = Entity.Create().add(UserComponent, { name: `User${i}` });
72
+ users.push(user);
73
+ }
74
+ await Promise.all(users.map(u => u.save()));
75
+
76
+ // Create 100 posts with random authors
77
+ const posts = [];
78
+ for (let i = 0; i < 100; i++) {
79
+ const randomUser = users[Math.floor(Math.random() * users.length)]!;
80
+ const post = Entity.Create()
81
+ .add(TitleComponent, { value: `Post${i}` })
82
+ .add(AuthorComponent, { value: randomUser.id });
83
+ posts.push(post);
84
+ }
85
+ await Promise.all(posts.map(p => p.save()));
86
+
87
+ // Test batched loading performance
88
+ const startTime = Date.now();
89
+ const loader = async (ids: string[]) => {
90
+ return await Entity.LoadMultiple(ids);
91
+ };
92
+
93
+ const result = await BatchLoader.loadRelatedEntitiesBatched(posts, AuthorComponent, loader);
94
+ const endTime = Date.now();
95
+
96
+ expect(result.size).toBeGreaterThan(0);
97
+ expect(endTime - startTime).toBeLessThan(1000); // Should complete under 1 second
98
+ });
99
+
100
+ test('isFieldRequestedSafe handles nested selections', () => {
101
+ const info = {
102
+ fieldNodes: [{
103
+ selectionSet: {
104
+ selections: [
105
+ { kind: 'Field', name: { value: 'id' } },
106
+ {
107
+ kind: 'Field',
108
+ name: { value: 'author' },
109
+ selectionSet: {
110
+ selections: [{ kind: 'Field', name: { value: 'name' } }]
111
+ }
112
+ }
113
+ ]
114
+ }
115
+ }]
116
+ };
117
+
118
+ expect(isFieldRequestedSafe(info, 'id')).toBe(true);
119
+ expect(isFieldRequestedSafe(info, 'author')).toBe(true);
120
+ expect(isFieldRequestedSafe(info, 'author', 'name')).toBe(true);
121
+ expect(isFieldRequestedSafe(info, 'title')).toBe(false);
122
+ });
123
+
124
+ test('isFieldRequestedSafe handles fragments', () => {
125
+ const info = {
126
+ fieldNodes: [{
127
+ selectionSet: {
128
+ selections: [
129
+ { kind: 'FragmentSpread', name: { value: 'UserFragment' } }
130
+ ]
131
+ }
132
+ }]
133
+ };
134
+
135
+ // Assuming fragments are expanded, but for test, mock as if selections are there
136
+ // In real scenario, fragments would be resolved
137
+ expect(isFieldRequestedSafe(info, 'id')).toBe(false); // since no direct field
138
+ });
139
+
140
+ test('isFieldRequestedSafe handles missing selectionSet', () => {
141
+ const info = {
142
+ fieldNodes: [{
143
+ // no selectionSet
144
+ }]
145
+ };
146
+
147
+ expect(isFieldRequestedSafe(info, 'id')).toBe(false);
148
+ });
149
+
150
+ test('Entity.doDelete resolves promise correctly', async () => {
151
+ const entity = Entity.Create().add(TitleComponent, { value: "Test" });
152
+ await entity.save();
153
+
154
+ const deletePromise = entity.delete();
155
+ await expect(deletePromise).resolves.toBe(true);
156
+
157
+ // Verify deleted (soft delete - check deleted_at is set)
158
+ const check = await db`SELECT id, deleted_at FROM entities WHERE id = ${entity.id}`;
159
+ expect(check.length).toBe(1);
160
+ expect(check[0].deleted_at).not.toBeNull();
161
+ });
162
+
163
+ test('Entity.doDelete resolves false for non-persisted entity', async () => {
164
+ const entity = new Entity(); // not saved
165
+
166
+ const deletePromise = entity.delete();
167
+ await expect(deletePromise).resolves.toBe(false);
168
+ });
169
+ });