bunsane 0.1.4 → 0.2.0
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/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ResolverBuilder
|
|
3
|
+
* Tests GraphQL resolver building
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
6
|
+
import { ResolverBuilder } from '../../../gql/builders/ResolverBuilder';
|
|
7
|
+
|
|
8
|
+
describe('ResolverBuilder', () => {
|
|
9
|
+
let builder: ResolverBuilder;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
builder = new ResolverBuilder();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('addResolver()', () => {
|
|
16
|
+
test('adds resolver to collection', () => {
|
|
17
|
+
const mockService = { myMethod: () => 'result' };
|
|
18
|
+
builder.addResolver({
|
|
19
|
+
name: 'myMethod',
|
|
20
|
+
propertyKey: 'myMethod',
|
|
21
|
+
type: 'Query',
|
|
22
|
+
service: mockService,
|
|
23
|
+
hasInput: false
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const stats = builder.getStats();
|
|
27
|
+
expect(stats.queries).toBe(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('categorizes Query resolvers', () => {
|
|
31
|
+
const mockService = { getUser: () => {} };
|
|
32
|
+
builder.addResolver({
|
|
33
|
+
name: 'getUser',
|
|
34
|
+
propertyKey: 'getUser',
|
|
35
|
+
type: 'Query',
|
|
36
|
+
service: mockService,
|
|
37
|
+
hasInput: false
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const stats = builder.getStats();
|
|
41
|
+
expect(stats.queries).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('categorizes Mutation resolvers', () => {
|
|
45
|
+
const mockService = { createUser: () => {} };
|
|
46
|
+
builder.addResolver({
|
|
47
|
+
name: 'createUser',
|
|
48
|
+
propertyKey: 'createUser',
|
|
49
|
+
type: 'Mutation',
|
|
50
|
+
service: mockService,
|
|
51
|
+
hasInput: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const stats = builder.getStats();
|
|
55
|
+
expect(stats.mutations).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('categorizes Subscription resolvers', () => {
|
|
59
|
+
const mockService = { userCreated: () => {} };
|
|
60
|
+
builder.addResolver({
|
|
61
|
+
name: 'userCreated',
|
|
62
|
+
propertyKey: 'userCreated',
|
|
63
|
+
type: 'Subscription',
|
|
64
|
+
service: mockService,
|
|
65
|
+
hasInput: false
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const stats = builder.getStats();
|
|
69
|
+
expect(stats.subscriptions).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('getResolvers()', () => {
|
|
74
|
+
test('returns empty object when no resolvers', () => {
|
|
75
|
+
const resolvers = builder.getResolvers();
|
|
76
|
+
expect(resolvers.Query).toEqual({});
|
|
77
|
+
expect(resolvers.Mutation).toEqual({});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('returns Query resolvers', () => {
|
|
81
|
+
const mockService = { getUser: () => 'user' };
|
|
82
|
+
builder.addResolver({
|
|
83
|
+
name: 'getUser',
|
|
84
|
+
propertyKey: 'getUser',
|
|
85
|
+
type: 'Query',
|
|
86
|
+
service: mockService,
|
|
87
|
+
hasInput: false
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const resolvers = builder.getResolvers();
|
|
91
|
+
expect(resolvers.Query!.getUser).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('returns Mutation resolvers', () => {
|
|
95
|
+
const mockService = { createUser: () => 'user' };
|
|
96
|
+
builder.addResolver({
|
|
97
|
+
name: 'createUser',
|
|
98
|
+
propertyKey: 'createUser',
|
|
99
|
+
type: 'Mutation',
|
|
100
|
+
service: mockService,
|
|
101
|
+
hasInput: true
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const resolvers = builder.getResolvers();
|
|
105
|
+
expect(resolvers.Mutation!.createUser).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('getResolversForType()', () => {
|
|
110
|
+
test('returns resolvers for specific type', () => {
|
|
111
|
+
const mockService = { getUser: () => {}, getUsers: () => {} };
|
|
112
|
+
builder.addResolver({
|
|
113
|
+
name: 'getUser',
|
|
114
|
+
propertyKey: 'getUser',
|
|
115
|
+
type: 'Query',
|
|
116
|
+
service: mockService,
|
|
117
|
+
hasInput: false
|
|
118
|
+
});
|
|
119
|
+
builder.addResolver({
|
|
120
|
+
name: 'getUsers',
|
|
121
|
+
propertyKey: 'getUsers',
|
|
122
|
+
type: 'Query',
|
|
123
|
+
service: mockService,
|
|
124
|
+
hasInput: false
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const queryResolvers = builder.getResolversForType('Query');
|
|
128
|
+
expect(Object.keys(queryResolvers).length).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('returns empty object for type with no resolvers', () => {
|
|
132
|
+
const resolvers = builder.getResolversForType('Subscription');
|
|
133
|
+
expect(resolvers).toEqual({});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('createResolverWithoutInput()', () => {
|
|
138
|
+
test('creates resolver function', () => {
|
|
139
|
+
const mockService = {
|
|
140
|
+
getUser: async () => ({ id: '1', name: 'Test' })
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const resolver = (builder as any).createResolverWithoutInput(mockService, 'getUser');
|
|
144
|
+
expect(typeof resolver).toBe('function');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('resolver calls service method', async () => {
|
|
148
|
+
let called = false;
|
|
149
|
+
const mockService = {
|
|
150
|
+
getUser: async () => {
|
|
151
|
+
called = true;
|
|
152
|
+
return { id: '1' };
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const resolver = (builder as any).createResolverWithoutInput(mockService, 'getUser');
|
|
157
|
+
await resolver({}, {}, {}, {} as any);
|
|
158
|
+
expect(called).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('createResolverWithInput()', () => {
|
|
163
|
+
test('creates resolver function with input handling', () => {
|
|
164
|
+
const mockService = {
|
|
165
|
+
createUser: async (input: any) => ({ id: '1', ...input })
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const resolver = (builder as any).createResolverWithInput(mockService, 'createUser');
|
|
169
|
+
expect(typeof resolver).toBe('function');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('resolver passes input to service', async () => {
|
|
173
|
+
let receivedInput: any = null;
|
|
174
|
+
const mockService = {
|
|
175
|
+
createUser: async (input: any) => {
|
|
176
|
+
receivedInput = input;
|
|
177
|
+
return { id: '1', ...input };
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const resolver = (builder as any).createResolverWithInput(mockService, 'createUser');
|
|
182
|
+
await resolver({}, { input: { name: 'Test' } }, {}, {} as any);
|
|
183
|
+
expect(receivedInput).toEqual({ name: 'Test' });
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('clear()', () => {
|
|
188
|
+
test('clears all resolvers', () => {
|
|
189
|
+
const mockService = { getUser: () => {} };
|
|
190
|
+
builder.addResolver({
|
|
191
|
+
name: 'getUser',
|
|
192
|
+
propertyKey: 'getUser',
|
|
193
|
+
type: 'Query',
|
|
194
|
+
service: mockService,
|
|
195
|
+
hasInput: false
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
builder.clear();
|
|
199
|
+
|
|
200
|
+
const stats = builder.getStats();
|
|
201
|
+
expect(stats.queries).toBe(0);
|
|
202
|
+
expect(stats.mutations).toBe(0);
|
|
203
|
+
expect(stats.subscriptions).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('getStats()', () => {
|
|
208
|
+
test('returns accurate statistics', () => {
|
|
209
|
+
const mockService = { m1: () => {}, m2: () => {}, m3: () => {} };
|
|
210
|
+
|
|
211
|
+
builder.addResolver({ name: 'q1', propertyKey: 'q1', type: 'Query', service: mockService, hasInput: false });
|
|
212
|
+
builder.addResolver({ name: 'q2', propertyKey: 'q2', type: 'Query', service: mockService, hasInput: false });
|
|
213
|
+
builder.addResolver({ name: 'm1', propertyKey: 'm1', type: 'Mutation', service: mockService, hasInput: true });
|
|
214
|
+
builder.addResolver({ name: 's1', propertyKey: 's1', type: 'Subscription', service: mockService, hasInput: false });
|
|
215
|
+
|
|
216
|
+
const stats = builder.getStats();
|
|
217
|
+
|
|
218
|
+
expect(stats.queries).toBe(2);
|
|
219
|
+
expect(stats.mutations).toBe(1);
|
|
220
|
+
expect(stats.subscriptions).toBe(1);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for TypeDefBuilder
|
|
3
|
+
* Tests GraphQL type definition building
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
6
|
+
import { TypeDefBuilder } from '../../../gql/builders/TypeDefBuilder';
|
|
7
|
+
|
|
8
|
+
describe('TypeDefBuilder', () => {
|
|
9
|
+
let builder: TypeDefBuilder;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
builder = new TypeDefBuilder();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('addQueryField()', () => {
|
|
16
|
+
test('adds query field', () => {
|
|
17
|
+
builder.addQueryField({ name: 'getUser', fieldDef: 'getUser(id: ID!): User' });
|
|
18
|
+
const stats = builder.getStats();
|
|
19
|
+
expect(stats.queries).toBe(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('adds multiple query fields', () => {
|
|
23
|
+
builder.addQueryField({ name: 'getUser', fieldDef: 'getUser(id: ID!): User' });
|
|
24
|
+
builder.addQueryField({ name: 'getUsers', fieldDef: 'getUsers: [User!]!' });
|
|
25
|
+
const stats = builder.getStats();
|
|
26
|
+
expect(stats.queries).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('addMutationField()', () => {
|
|
31
|
+
test('adds mutation field', () => {
|
|
32
|
+
builder.addMutationField({ name: 'createUser', fieldDef: 'createUser(input: CreateUserInput!): User!' });
|
|
33
|
+
const stats = builder.getStats();
|
|
34
|
+
expect(stats.mutations).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('addSubscriptionField()', () => {
|
|
39
|
+
test('adds subscription field', () => {
|
|
40
|
+
builder.addSubscriptionField({ name: 'userCreated', fieldDef: 'userCreated: User!' });
|
|
41
|
+
const stats = builder.getStats();
|
|
42
|
+
expect(stats.subscriptions).toBe(1);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('buildQueryType()', () => {
|
|
47
|
+
test('returns empty string when no queries', () => {
|
|
48
|
+
const result = builder.buildQueryType();
|
|
49
|
+
expect(result).toBe('');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('builds Query type with fields', () => {
|
|
53
|
+
builder.addQueryField({ name: 'getUser', fieldDef: 'getUser(id: ID!): User' });
|
|
54
|
+
const result = builder.buildQueryType();
|
|
55
|
+
|
|
56
|
+
expect(result).toContain('type Query');
|
|
57
|
+
expect(result).toContain('getUser(id: ID!): User');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('sorts fields alphabetically', () => {
|
|
61
|
+
builder.addQueryField({ name: 'zebra', fieldDef: 'zebra: String' });
|
|
62
|
+
builder.addQueryField({ name: 'alpha', fieldDef: 'alpha: String' });
|
|
63
|
+
const result = builder.buildQueryType();
|
|
64
|
+
|
|
65
|
+
const alphaIndex = result.indexOf('alpha');
|
|
66
|
+
const zebraIndex = result.indexOf('zebra');
|
|
67
|
+
expect(alphaIndex).toBeLessThan(zebraIndex);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('buildMutationType()', () => {
|
|
72
|
+
test('returns empty string when no mutations', () => {
|
|
73
|
+
const result = builder.buildMutationType();
|
|
74
|
+
expect(result).toBe('');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('builds Mutation type with fields', () => {
|
|
78
|
+
builder.addMutationField({ name: 'createUser', fieldDef: 'createUser(input: CreateUserInput!): User!' });
|
|
79
|
+
const result = builder.buildMutationType();
|
|
80
|
+
|
|
81
|
+
expect(result).toContain('type Mutation');
|
|
82
|
+
expect(result).toContain('createUser');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('buildSubscriptionType()', () => {
|
|
87
|
+
test('returns empty string when no subscriptions', () => {
|
|
88
|
+
const result = builder.buildSubscriptionType();
|
|
89
|
+
expect(result).toBe('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('builds Subscription type with fields', () => {
|
|
93
|
+
builder.addSubscriptionField({ name: 'userCreated', fieldDef: 'userCreated: User!' });
|
|
94
|
+
const result = builder.buildSubscriptionType();
|
|
95
|
+
|
|
96
|
+
expect(result).toContain('type Subscription');
|
|
97
|
+
expect(result).toContain('userCreated');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('buildAllOperationTypes()', () => {
|
|
102
|
+
test('builds all operation types', () => {
|
|
103
|
+
builder.addQueryField({ name: 'getUser', fieldDef: 'getUser(id: ID!): User' });
|
|
104
|
+
builder.addMutationField({ name: 'createUser', fieldDef: 'createUser(input: CreateUserInput!): User!' });
|
|
105
|
+
builder.addSubscriptionField({ name: 'userCreated', fieldDef: 'userCreated: User!' });
|
|
106
|
+
|
|
107
|
+
const result = builder.buildAllOperationTypes();
|
|
108
|
+
|
|
109
|
+
expect(result).toContain('type Query');
|
|
110
|
+
expect(result).toContain('type Mutation');
|
|
111
|
+
expect(result).toContain('type Subscription');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('returns only defined types', () => {
|
|
115
|
+
builder.addQueryField({ name: 'getUser', fieldDef: 'getUser(id: ID!): User' });
|
|
116
|
+
|
|
117
|
+
const result = builder.buildAllOperationTypes();
|
|
118
|
+
|
|
119
|
+
expect(result).toContain('type Query');
|
|
120
|
+
expect(result).not.toContain('type Mutation');
|
|
121
|
+
expect(result).not.toContain('type Subscription');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('clear()', () => {
|
|
126
|
+
test('clears all fields', () => {
|
|
127
|
+
builder.addQueryField({ name: 'getUser', fieldDef: 'getUser: User' });
|
|
128
|
+
builder.addMutationField({ name: 'createUser', fieldDef: 'createUser: User' });
|
|
129
|
+
builder.addSubscriptionField({ name: 'userCreated', fieldDef: 'userCreated: User' });
|
|
130
|
+
|
|
131
|
+
builder.clear();
|
|
132
|
+
|
|
133
|
+
const stats = builder.getStats();
|
|
134
|
+
expect(stats.queries).toBe(0);
|
|
135
|
+
expect(stats.mutations).toBe(0);
|
|
136
|
+
expect(stats.subscriptions).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('getStats()', () => {
|
|
141
|
+
test('returns correct counts', () => {
|
|
142
|
+
builder.addQueryField({ name: 'q1', fieldDef: 'q1: String' });
|
|
143
|
+
builder.addQueryField({ name: 'q2', fieldDef: 'q2: String' });
|
|
144
|
+
builder.addMutationField({ name: 'm1', fieldDef: 'm1: String' });
|
|
145
|
+
|
|
146
|
+
const stats = builder.getStats();
|
|
147
|
+
|
|
148
|
+
expect(stats.queries).toBe(2);
|
|
149
|
+
expect(stats.mutations).toBe(1);
|
|
150
|
+
expect(stats.subscriptions).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ArcheType persistence
|
|
3
|
+
* Tests archetype creation and loading with the database
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, beforeAll, beforeEach, afterEach } from 'bun:test';
|
|
6
|
+
import { Entity } from '../../../core/Entity';
|
|
7
|
+
import { Query, FilterOp } from '../../../query/Query';
|
|
8
|
+
import { TestUser, TestProduct, TestOrder } from '../../fixtures/components';
|
|
9
|
+
import { TestUserArchetype, TestUserWithOrdersArchetype } from '../../fixtures/archetypes/TestUserArchetype';
|
|
10
|
+
import { createTestContext, ensureComponentsRegistered } from '../../utils';
|
|
11
|
+
|
|
12
|
+
describe('ArcheType Persistence', () => {
|
|
13
|
+
const ctx = createTestContext();
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
await ensureComponentsRegistered(TestUser, TestProduct, TestOrder);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('createAndSaveEntity()', () => {
|
|
20
|
+
test('creates and persists entity with archetype components', async () => {
|
|
21
|
+
const archetype = new TestUserArchetype();
|
|
22
|
+
archetype.fill({ user: { name: 'ArchetypeSave', email: 'archsave@example.com', age: 30 } });
|
|
23
|
+
const entity = await archetype.createAndSaveEntity();
|
|
24
|
+
ctx.tracker.track(entity);
|
|
25
|
+
|
|
26
|
+
expect(entity._persisted).toBe(true);
|
|
27
|
+
|
|
28
|
+
const loaded = await Entity.FindById(entity.id);
|
|
29
|
+
expect(loaded).not.toBeNull();
|
|
30
|
+
|
|
31
|
+
// Use async get() since component may not be in memory after FindById
|
|
32
|
+
const userData = await loaded?.get(TestUser);
|
|
33
|
+
expect(userData?.name).toBe('ArchetypeSave');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('creates entity with multiple components', async () => {
|
|
37
|
+
const archetype = new TestUserWithOrdersArchetype();
|
|
38
|
+
archetype.fill({
|
|
39
|
+
user: { name: 'MultiArch', email: 'multiarch@example.com', age: 28 },
|
|
40
|
+
order: {
|
|
41
|
+
orderNumber: 'ORD-ARCH-001',
|
|
42
|
+
total: 199.99,
|
|
43
|
+
status: 'completed',
|
|
44
|
+
createdAt: new Date()
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const entity = await archetype.createAndSaveEntity();
|
|
48
|
+
ctx.tracker.track(entity);
|
|
49
|
+
|
|
50
|
+
const loaded = await Entity.FindById(entity.id);
|
|
51
|
+
expect(loaded).not.toBeNull();
|
|
52
|
+
|
|
53
|
+
// Load components and verify
|
|
54
|
+
const userData = await loaded?.get(TestUser);
|
|
55
|
+
const orderData = await loaded?.get(TestOrder);
|
|
56
|
+
expect(userData).toBeDefined();
|
|
57
|
+
expect(orderData).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('createEntity() with fill()', () => {
|
|
62
|
+
test('creates entity from archetype with data', async () => {
|
|
63
|
+
const archetype = new TestUserArchetype();
|
|
64
|
+
archetype.fill({ user: { name: 'FillCreate', email: 'fillcreate@example.com', age: 25 } });
|
|
65
|
+
const entity = archetype.createEntity();
|
|
66
|
+
ctx.tracker.track(entity);
|
|
67
|
+
|
|
68
|
+
expect(entity).toBeInstanceOf(Entity);
|
|
69
|
+
expect(entity.id).toBeDefined();
|
|
70
|
+
expect((entity as any)._dirty).toBe(true);
|
|
71
|
+
expect(entity._persisted).toBe(false);
|
|
72
|
+
|
|
73
|
+
// Component should be in memory after createEntity
|
|
74
|
+
const userData = entity.getInMemory(TestUser);
|
|
75
|
+
expect(userData?.name).toBe('FillCreate');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('entity can be saved after creation', async () => {
|
|
79
|
+
const archetype = new TestUserArchetype();
|
|
80
|
+
archetype.fill({ user: { name: 'SaveAfter', email: 'saveafter@example.com', age: 30 } });
|
|
81
|
+
const entity = archetype.createEntity();
|
|
82
|
+
ctx.tracker.track(entity);
|
|
83
|
+
|
|
84
|
+
await entity.save();
|
|
85
|
+
|
|
86
|
+
expect(entity._persisted).toBe(true);
|
|
87
|
+
expect((entity as any)._dirty).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('getEntityWithID()', () => {
|
|
92
|
+
test('loads entity by ID with archetype components', async () => {
|
|
93
|
+
// Create and save an entity
|
|
94
|
+
const archetype = new TestUserArchetype();
|
|
95
|
+
archetype.fill({ user: { name: 'GetWithId', email: 'getwithid@example.com', age: 25 } });
|
|
96
|
+
const entity = await archetype.createAndSaveEntity();
|
|
97
|
+
ctx.tracker.track(entity);
|
|
98
|
+
|
|
99
|
+
// Load using archetype's getEntityWithID
|
|
100
|
+
const loadArchetype = new TestUserArchetype();
|
|
101
|
+
const loaded = await loadArchetype.getEntityWithID(entity.id);
|
|
102
|
+
|
|
103
|
+
expect(loaded).not.toBeNull();
|
|
104
|
+
expect(loaded?.id).toBe(entity.id);
|
|
105
|
+
|
|
106
|
+
// Component should be loaded
|
|
107
|
+
const userData = await loaded?.get(TestUser);
|
|
108
|
+
expect(userData?.name).toBe('GetWithId');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('returns null for non-existent ID', async () => {
|
|
112
|
+
const archetype = new TestUserArchetype();
|
|
113
|
+
const loaded = await archetype.getEntityWithID('00000000-0000-0000-0000-000000000000');
|
|
114
|
+
expect(loaded).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('returns null for invalid ID', async () => {
|
|
118
|
+
const archetype = new TestUserArchetype();
|
|
119
|
+
const loaded = await archetype.getEntityWithID('');
|
|
120
|
+
expect(loaded).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('updateEntity()', () => {
|
|
125
|
+
test('updates entity with new data', async () => {
|
|
126
|
+
const archetype = new TestUserArchetype();
|
|
127
|
+
archetype.fill({ user: { name: 'ToUpdate', email: 'toupdate@example.com', age: 30 } });
|
|
128
|
+
const entity = await archetype.createAndSaveEntity();
|
|
129
|
+
ctx.tracker.track(entity);
|
|
130
|
+
|
|
131
|
+
// Update the entity
|
|
132
|
+
await archetype.updateEntity(entity, {
|
|
133
|
+
user: { name: 'Updated', age: 31 }
|
|
134
|
+
});
|
|
135
|
+
await entity.save();
|
|
136
|
+
|
|
137
|
+
const loaded = await Entity.FindById(entity.id);
|
|
138
|
+
const userData = await loaded?.get(TestUser);
|
|
139
|
+
expect(userData?.name).toBe('Updated');
|
|
140
|
+
expect(userData?.age).toBe(31);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('preserves unchanged fields', async () => {
|
|
144
|
+
const archetype = new TestUserArchetype();
|
|
145
|
+
archetype.fill({ user: { name: 'Preserve', email: 'preserve@example.com', age: 25 } });
|
|
146
|
+
const entity = await archetype.createAndSaveEntity();
|
|
147
|
+
ctx.tracker.track(entity);
|
|
148
|
+
|
|
149
|
+
// Update only name
|
|
150
|
+
await archetype.updateEntity(entity, {
|
|
151
|
+
user: { name: 'PreserveUpdated' }
|
|
152
|
+
});
|
|
153
|
+
await entity.save();
|
|
154
|
+
|
|
155
|
+
const loaded = await Entity.FindById(entity.id);
|
|
156
|
+
const userData = await loaded?.get(TestUser);
|
|
157
|
+
expect(userData?.name).toBe('PreserveUpdated');
|
|
158
|
+
expect(userData?.email).toBe('preserve@example.com'); // Email unchanged
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('querying entities with archetype components', () => {
|
|
163
|
+
beforeEach(async () => {
|
|
164
|
+
// Create test data
|
|
165
|
+
for (let i = 0; i < 3; i++) {
|
|
166
|
+
const archetype = new TestUserArchetype();
|
|
167
|
+
archetype.fill({
|
|
168
|
+
user: {
|
|
169
|
+
name: `QueryArchUser${i}`,
|
|
170
|
+
email: `queryarch${i}@example.com`,
|
|
171
|
+
age: 20 + i * 10
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const entity = await archetype.createAndSaveEntity();
|
|
175
|
+
ctx.tracker.track(entity);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('finds entities via Query with archetype components', async () => {
|
|
180
|
+
const results = await new Query()
|
|
181
|
+
.with(TestUser, {
|
|
182
|
+
filters: [Query.filter('email', FilterOp.LIKE, 'queryarch%@example.com')]
|
|
183
|
+
})
|
|
184
|
+
.populate()
|
|
185
|
+
.exec();
|
|
186
|
+
|
|
187
|
+
expect(results.length).toBeGreaterThanOrEqual(3);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('filters by component field values', async () => {
|
|
191
|
+
const results = await new Query()
|
|
192
|
+
.with(TestUser, {
|
|
193
|
+
filters: [Query.filter('name', FilterOp.EQ, 'QueryArchUser0')]
|
|
194
|
+
})
|
|
195
|
+
.populate()
|
|
196
|
+
.exec();
|
|
197
|
+
|
|
198
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
199
|
+
const userData = await results[0]?.get(TestUser);
|
|
200
|
+
expect(userData?.name).toBe('QueryArchUser0');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('Unwrap()', () => {
|
|
205
|
+
test('unwraps entity to plain object', async () => {
|
|
206
|
+
const archetype = new TestUserArchetype();
|
|
207
|
+
archetype.fill({ user: { name: 'Unwrap', email: 'unwrap@example.com', age: 30 } });
|
|
208
|
+
const entity = await archetype.createAndSaveEntity();
|
|
209
|
+
ctx.tracker.track(entity);
|
|
210
|
+
|
|
211
|
+
const unwrapped = await archetype.Unwrap(entity);
|
|
212
|
+
|
|
213
|
+
expect(unwrapped.id).toBe(entity.id);
|
|
214
|
+
// The unwrapped format may vary - check that user data is present
|
|
215
|
+
expect(unwrapped.user || unwrapped).toBeDefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('validation', () => {
|
|
220
|
+
test('withValidation validates input data', () => {
|
|
221
|
+
const archetype = new TestUserArchetype();
|
|
222
|
+
|
|
223
|
+
// Valid data should pass
|
|
224
|
+
const validResult = archetype.withValidation({
|
|
225
|
+
user: { name: 'Valid', email: 'valid@example.com', age: 25 }
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(validResult).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('component properties', () => {
|
|
233
|
+
test('getComponentsToLoad returns component constructors', () => {
|
|
234
|
+
const archetype = new TestUserArchetype();
|
|
235
|
+
const components = (archetype as any).getComponentsToLoad();
|
|
236
|
+
|
|
237
|
+
expect(Array.isArray(components)).toBe(true);
|
|
238
|
+
expect(components.length).toBeGreaterThan(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|