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.
Files changed (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. 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
+ });