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,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
|
+
});
|