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,124 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Loader2, AlertCircle, Layers, Database, ChevronDown, ChevronRight } from 'lucide-react'
3
+ import { PageContainer } from '../components/PageContainer'
4
+ import { PageHeader } from '../components/PageHeader'
5
+ import { fetchComponents, type ComponentTypeInfo } from '../lib/api'
6
+ import { toast } from 'sonner'
7
+
8
+ export function Components() {
9
+ const [components, setComponents] = useState<ComponentTypeInfo[]>([])
10
+ const [loading, setLoading] = useState(true)
11
+ const [error, setError] = useState<string | null>(null)
12
+
13
+ useEffect(() => {
14
+ const load = async () => {
15
+ setLoading(true)
16
+ setError(null)
17
+ try {
18
+ const data = await fetchComponents()
19
+ setComponents(data)
20
+ } catch (err) {
21
+ const message =
22
+ err instanceof Error ? err.message : 'Failed to fetch components'
23
+ setError(message)
24
+ toast.error(message)
25
+ } finally {
26
+ setLoading(false)
27
+ }
28
+ }
29
+ load()
30
+ }, [])
31
+
32
+ return (
33
+ <PageContainer>
34
+ <PageHeader
35
+ title="Component Types"
36
+ description="All distinct component types in your ECS database with field shapes and entity counts."
37
+ />
38
+
39
+ {loading && (
40
+ <div className="flex items-center gap-2 text-muted-foreground py-12 justify-center">
41
+ <Loader2 className="h-5 w-5 animate-spin" />
42
+ <span>Loading components...</span>
43
+ </div>
44
+ )}
45
+
46
+ {error && !loading && (
47
+ <div className="flex items-center gap-2 text-destructive bg-destructive/10 border border-destructive/20 rounded-md p-4 mb-6">
48
+ <AlertCircle className="h-5 w-5 shrink-0" />
49
+ <span>{error}</span>
50
+ </div>
51
+ )}
52
+
53
+ {!loading && !error && components.length === 0 && (
54
+ <div className="text-center py-16 text-muted-foreground">
55
+ <Layers className="h-12 w-12 mx-auto mb-4 opacity-50" />
56
+ <p className="text-lg">No component types found</p>
57
+ </div>
58
+ )}
59
+
60
+ {!loading && components.length > 0 && (
61
+ <div className="space-y-3">
62
+ {components.map((comp) => (
63
+ <ComponentRow key={comp.name} component={comp} />
64
+ ))}
65
+ </div>
66
+ )}
67
+ </PageContainer>
68
+ )
69
+ }
70
+
71
+ function ComponentRow({ component }: { component: ComponentTypeInfo }) {
72
+ const [expanded, setExpanded] = useState(false)
73
+
74
+ return (
75
+ <div className="border border-border rounded-lg bg-card overflow-hidden">
76
+ <button
77
+ onClick={() => setExpanded(!expanded)}
78
+ className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-accent/50 transition-colors"
79
+ >
80
+ <div className="flex items-center gap-3">
81
+ {expanded ? (
82
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
83
+ ) : (
84
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
85
+ )}
86
+ <span className="font-semibold text-sm">{component.name}</span>
87
+ <span className="text-xs text-muted-foreground">
88
+ {component.entityCount.toLocaleString()} {component.entityCount === 1 ? 'entity' : 'entities'}
89
+ </span>
90
+ </div>
91
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
92
+ <Database className="h-3 w-3" />
93
+ <span className="font-mono">{component.partitionTable}</span>
94
+ </div>
95
+ </button>
96
+
97
+ {expanded && (
98
+ <div className="border-t border-border px-5 py-4">
99
+ {component.fields.length > 0 ? (
100
+ <div>
101
+ <p className="text-xs text-muted-foreground mb-2 font-medium">
102
+ JSONB Fields ({component.fields.length})
103
+ </p>
104
+ <div className="flex flex-wrap gap-2">
105
+ {component.fields.map((field) => (
106
+ <span
107
+ key={field}
108
+ className="inline-block px-2.5 py-1 rounded-md bg-muted text-sm font-mono"
109
+ >
110
+ {field}
111
+ </span>
112
+ ))}
113
+ </div>
114
+ </div>
115
+ ) : (
116
+ <p className="text-sm text-muted-foreground">
117
+ No field data available (component data may be empty or non-object).
118
+ </p>
119
+ )}
120
+ </div>
121
+ )}
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,302 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useParams, useNavigate, Link } from 'react-router-dom'
3
+ import { Search, Loader2, AlertCircle, Clock, Hash, Trash2 } from 'lucide-react'
4
+ import ReactJson from 'react-json-view'
5
+ import { PageContainer } from '../components/PageContainer'
6
+ import { PageHeader } from '../components/PageHeader'
7
+ import { Input } from '../components/ui/input'
8
+ import { Button } from '../components/ui/button'
9
+ import { fetchEntity, type EntityInspectorData } from '../lib/api'
10
+ import { useStudioStore, type Metadata } from '../store/studio'
11
+ import { cn } from '../lib/utils'
12
+ import { toast } from 'sonner'
13
+
14
+ function formatDate(dateStr: string | null): string {
15
+ if (!dateStr) return '-'
16
+ return new Date(dateStr).toLocaleString()
17
+ }
18
+
19
+ function deriveArchetypes(
20
+ componentNames: string[],
21
+ metadata: Metadata | null
22
+ ): { name: string; path: string }[] {
23
+ if (!metadata?.archeTypes) return []
24
+
25
+ const activeNames = new Set(componentNames)
26
+ const matches: { name: string; path: string }[] = []
27
+
28
+ for (const [name, fields] of Object.entries(metadata.archeTypes)) {
29
+ const required = fields
30
+ .filter((f) => !f.nullable)
31
+ .map((f) => f.componentName)
32
+ if (required.length > 0 && required.every((c) => activeNames.has(c))) {
33
+ matches.push({ name, path: `/archetype/${name}` })
34
+ }
35
+ }
36
+
37
+ return matches
38
+ }
39
+
40
+ export function EntityInspector() {
41
+ const { id } = useParams<{ id: string }>()
42
+ const navigate = useNavigate()
43
+ const { metadata } = useStudioStore()
44
+
45
+ const [searchInput, setSearchInput] = useState(id ?? '')
46
+ const [data, setData] = useState<EntityInspectorData | null>(null)
47
+ const [loading, setLoading] = useState(false)
48
+ const [error, setError] = useState<string | null>(null)
49
+
50
+ const loadEntity = async (entityId: string) => {
51
+ setLoading(true)
52
+ setError(null)
53
+ setData(null)
54
+ try {
55
+ const result = await fetchEntity(entityId)
56
+ setData(result)
57
+ } catch (err) {
58
+ const message = err instanceof Error ? err.message : 'Failed to fetch entity'
59
+ setError(message)
60
+ toast.error(message)
61
+ } finally {
62
+ setLoading(false)
63
+ }
64
+ }
65
+
66
+ useEffect(() => {
67
+ if (id) {
68
+ setSearchInput(id)
69
+ loadEntity(id)
70
+ }
71
+ }, [id])
72
+
73
+ const handleSubmit = (e: React.FormEvent) => {
74
+ e.preventDefault()
75
+ const trimmed = searchInput.trim()
76
+ if (!trimmed) return
77
+ navigate(`/entity/${trimmed}`)
78
+ }
79
+
80
+ const activeComponents = data
81
+ ? data.components.filter((c) => !c.deleted_at)
82
+ : []
83
+ const deletedComponents = data
84
+ ? data.components.filter((c) => c.deleted_at)
85
+ : []
86
+
87
+ const archetypes = data
88
+ ? deriveArchetypes(
89
+ activeComponents.map((c) => c.name),
90
+ metadata
91
+ )
92
+ : []
93
+
94
+ return (
95
+ <PageContainer>
96
+ <PageHeader
97
+ title="Entity Inspector"
98
+ description="Look up an entity by ID to see all its components and metadata."
99
+ />
100
+
101
+ {/* Search bar */}
102
+ <form onSubmit={handleSubmit} className="flex gap-2 mb-8 max-w-2xl">
103
+ <div className="relative flex-1">
104
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
105
+ <Input
106
+ value={searchInput}
107
+ onChange={(e) => setSearchInput(e.target.value)}
108
+ placeholder="Paste entity UUID..."
109
+ className="pl-9 font-mono"
110
+ />
111
+ </div>
112
+ <Button type="submit" disabled={loading || !searchInput.trim()}>
113
+ {loading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Inspect'}
114
+ </Button>
115
+ </form>
116
+
117
+ {/* Loading */}
118
+ {loading && (
119
+ <div className="flex items-center gap-2 text-muted-foreground">
120
+ <Loader2 className="h-5 w-5 animate-spin" />
121
+ <span>Loading entity...</span>
122
+ </div>
123
+ )}
124
+
125
+ {/* Error */}
126
+ {error && !loading && (
127
+ <div className="flex items-center gap-2 text-destructive bg-destructive/10 border border-destructive/20 rounded-md p-4">
128
+ <AlertCircle className="h-5 w-5 shrink-0" />
129
+ <span>{error}</span>
130
+ </div>
131
+ )}
132
+
133
+ {/* Results */}
134
+ {data && !loading && (
135
+ <div className="space-y-6">
136
+ {/* Entity metadata card */}
137
+ <div className="border border-border rounded-lg p-6 bg-card">
138
+ <h2 className="text-lg font-semibold mb-4">Entity</h2>
139
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
140
+ <div>
141
+ <p className="text-xs text-muted-foreground mb-1">ID</p>
142
+ <p className="font-mono text-sm break-all">{data.entity.id}</p>
143
+ </div>
144
+ <div>
145
+ <p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
146
+ <Clock className="h-3 w-3" /> Created
147
+ </p>
148
+ <p className="text-sm">{formatDate(data.entity.created_at)}</p>
149
+ </div>
150
+ <div>
151
+ <p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
152
+ <Clock className="h-3 w-3" /> Updated
153
+ </p>
154
+ <p className="text-sm">{formatDate(data.entity.updated_at)}</p>
155
+ </div>
156
+ <div>
157
+ <p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
158
+ <Trash2 className="h-3 w-3" /> Deleted
159
+ </p>
160
+ {data.entity.deleted_at ? (
161
+ <span className="inline-flex items-center gap-1 text-sm text-destructive font-medium">
162
+ {formatDate(data.entity.deleted_at)}
163
+ </span>
164
+ ) : (
165
+ <span className="text-sm text-muted-foreground">-</span>
166
+ )}
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ {/* Archetype membership */}
172
+ {archetypes.length > 0 && (
173
+ <div className="border border-border rounded-lg p-6 bg-card">
174
+ <h2 className="text-lg font-semibold mb-3">Archetype Membership</h2>
175
+ <div className="flex flex-wrap gap-2">
176
+ {archetypes.map((a) => (
177
+ <Link
178
+ key={a.name}
179
+ to={a.path}
180
+ className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
181
+ >
182
+ {a.name}
183
+ </Link>
184
+ ))}
185
+ </div>
186
+ </div>
187
+ )}
188
+
189
+ {/* Active components */}
190
+ <div>
191
+ <h2 className="text-lg font-semibold mb-3">
192
+ Components ({activeComponents.length})
193
+ </h2>
194
+ {activeComponents.length === 0 ? (
195
+ <p className="text-muted-foreground text-sm">No active components.</p>
196
+ ) : (
197
+ <div className="space-y-3">
198
+ {activeComponents.map((comp) => (
199
+ <ComponentCard key={comp.id} component={comp} />
200
+ ))}
201
+ </div>
202
+ )}
203
+ </div>
204
+
205
+ {/* Deleted components */}
206
+ {deletedComponents.length > 0 && (
207
+ <div>
208
+ <h2 className="text-lg font-semibold mb-3 text-destructive">
209
+ Deleted Components ({deletedComponents.length})
210
+ </h2>
211
+ <div className="space-y-3">
212
+ {deletedComponents.map((comp) => (
213
+ <ComponentCard key={comp.id} component={comp} deleted />
214
+ ))}
215
+ </div>
216
+ </div>
217
+ )}
218
+ </div>
219
+ )}
220
+
221
+ {/* Empty state */}
222
+ {!data && !loading && !error && !id && (
223
+ <div className="text-center py-16 text-muted-foreground">
224
+ <Search className="h-12 w-12 mx-auto mb-4 opacity-50" />
225
+ <p className="text-lg">Enter an entity UUID above to inspect it</p>
226
+ </div>
227
+ )}
228
+ </PageContainer>
229
+ )
230
+ }
231
+
232
+ function ComponentCard({
233
+ component,
234
+ deleted = false,
235
+ }: {
236
+ component: EntityInspectorData['components'][number]
237
+ deleted?: boolean
238
+ }) {
239
+ const [expanded, setExpanded] = useState(true)
240
+
241
+ return (
242
+ <div
243
+ className={cn(
244
+ 'border rounded-lg overflow-hidden',
245
+ deleted
246
+ ? 'border-destructive/30 bg-destructive/5 opacity-75'
247
+ : 'border-border bg-card'
248
+ )}
249
+ >
250
+ <button
251
+ onClick={() => setExpanded(!expanded)}
252
+ className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-accent/50 transition-colors"
253
+ >
254
+ <div className="flex items-center gap-3">
255
+ <span className="font-semibold text-sm">{component.name}</span>
256
+ {deleted && (
257
+ <span className="text-xs bg-destructive text-destructive-foreground px-2 py-0.5 rounded-full">
258
+ Deleted
259
+ </span>
260
+ )}
261
+ </div>
262
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
263
+ <span className="flex items-center gap-1" title="type_id">
264
+ <Hash className="h-3 w-3" />
265
+ {component.type_id.slice(0, 8)}...
266
+ </span>
267
+ <span>{expanded ? '−' : '+'}</span>
268
+ </div>
269
+ </button>
270
+ {expanded && (
271
+ <div className="border-t border-border px-4 py-3 space-y-3">
272
+ {/* Timestamps */}
273
+ <div className="flex gap-6 text-xs text-muted-foreground">
274
+ <span>Created: {formatDate(component.created_at)}</span>
275
+ <span>Updated: {formatDate(component.updated_at)}</span>
276
+ {component.deleted_at && (
277
+ <span className="text-destructive">
278
+ Deleted: {formatDate(component.deleted_at)}
279
+ </span>
280
+ )}
281
+ </div>
282
+ {/* Data */}
283
+ {component.data && typeof component.data === 'object' ? (
284
+ <ReactJson
285
+ src={component.data as object}
286
+ name={null}
287
+ collapsed={false}
288
+ displayDataTypes={false}
289
+ enableClipboard
290
+ theme="rjv-default"
291
+ style={{ fontSize: '13px' }}
292
+ />
293
+ ) : (
294
+ <pre className="text-sm font-mono bg-muted p-3 rounded-md overflow-auto">
295
+ {JSON.stringify(component.data, null, 2)}
296
+ </pre>
297
+ )}
298
+ </div>
299
+ )}
300
+ </div>
301
+ )
302
+ }
@@ -0,0 +1,246 @@
1
+ import { useState } from 'react'
2
+ import { Link } from 'react-router-dom'
3
+ import { Loader2, AlertCircle, Play, Clock, ChevronDown } from 'lucide-react'
4
+ import { PageContainer } from '../components/PageContainer'
5
+ import { PageHeader } from '../components/PageHeader'
6
+ import { Button } from '../components/ui/button'
7
+ import { executeQuery, type QueryResult } from '../lib/api'
8
+ import { toast } from 'sonner'
9
+
10
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
11
+
12
+ const TEMPLATES = [
13
+ {
14
+ label: 'Entities with component',
15
+ sql: `SELECT e.id, e.created_at, c.name as component, c.data
16
+ FROM entities e
17
+ JOIN components c ON c.entity_id = e.id
18
+ WHERE c.name = 'MyComponent'
19
+ AND c.deleted_at IS NULL
20
+ ORDER BY e.created_at DESC
21
+ LIMIT 50`,
22
+ },
23
+ {
24
+ label: 'Orphaned components',
25
+ sql: `SELECT c.id, c.entity_id, c.name, c.created_at
26
+ FROM components c
27
+ LEFT JOIN entities e ON e.id = c.entity_id
28
+ WHERE e.id IS NULL
29
+ LIMIT 50`,
30
+ },
31
+ {
32
+ label: 'Recently deleted entities',
33
+ sql: `SELECT id, created_at, deleted_at
34
+ FROM entities
35
+ WHERE deleted_at IS NOT NULL
36
+ ORDER BY deleted_at DESC
37
+ LIMIT 50`,
38
+ },
39
+ {
40
+ label: 'Component data for entity',
41
+ sql: `SELECT c.name, c.type_id, c.data, c.created_at, c.updated_at, c.deleted_at
42
+ FROM components c
43
+ WHERE c.entity_id = '00000000-0000-0000-0000-000000000000'
44
+ ORDER BY c.name`,
45
+ },
46
+ {
47
+ label: 'Component type counts',
48
+ sql: `SELECT name, COUNT(*) as count
49
+ FROM components
50
+ WHERE deleted_at IS NULL
51
+ GROUP BY name
52
+ ORDER BY count DESC`,
53
+ },
54
+ {
55
+ label: 'Table sizes',
56
+ sql: `SELECT relname AS table_name,
57
+ pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
58
+ pg_size_pretty(pg_relation_size(relid)) AS data_size,
59
+ n_live_tup AS row_estimate
60
+ FROM pg_stat_user_tables
61
+ ORDER BY pg_total_relation_size(relid) DESC`,
62
+ },
63
+ ]
64
+
65
+ export function QueryRunner() {
66
+ const [sql, setSql] = useState('')
67
+ const [result, setResult] = useState<QueryResult | null>(null)
68
+ const [loading, setLoading] = useState(false)
69
+ const [error, setError] = useState<string | null>(null)
70
+ const [showTemplates, setShowTemplates] = useState(false)
71
+
72
+ const handleRun = async () => {
73
+ const trimmed = sql.trim()
74
+ if (!trimmed) return
75
+
76
+ setLoading(true)
77
+ setError(null)
78
+ setResult(null)
79
+ try {
80
+ const data = await executeQuery(trimmed)
81
+ setResult(data)
82
+ } catch (err) {
83
+ const message = err instanceof Error ? err.message : 'Query failed'
84
+ setError(message)
85
+ toast.error(message)
86
+ } finally {
87
+ setLoading(false)
88
+ }
89
+ }
90
+
91
+ const handleKeyDown = (e: React.KeyboardEvent) => {
92
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
93
+ e.preventDefault()
94
+ handleRun()
95
+ }
96
+ }
97
+
98
+ return (
99
+ <PageContainer>
100
+ <PageHeader
101
+ title="SQL Query Runner"
102
+ description="Execute read-only SQL queries against the database. Dev mode only."
103
+ />
104
+
105
+ {/* SQL editor */}
106
+ <div className="space-y-3 mb-6">
107
+ <div className="flex items-center gap-2">
108
+ <div className="relative">
109
+ <Button
110
+ variant="outline"
111
+ size="sm"
112
+ onClick={() => setShowTemplates(!showTemplates)}
113
+ >
114
+ Templates
115
+ <ChevronDown className="h-3 w-3 ml-1" />
116
+ </Button>
117
+ {showTemplates && (
118
+ <div className="absolute top-full left-0 mt-1 z-10 bg-card border border-border rounded-md shadow-lg min-w-[220px]">
119
+ {TEMPLATES.map((t) => (
120
+ <button
121
+ key={t.label}
122
+ className="w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors first:rounded-t-md last:rounded-b-md"
123
+ onClick={() => {
124
+ setSql(t.sql)
125
+ setShowTemplates(false)
126
+ }}
127
+ >
128
+ {t.label}
129
+ </button>
130
+ ))}
131
+ </div>
132
+ )}
133
+ </div>
134
+ <span className="text-xs text-muted-foreground">
135
+ Ctrl+Enter to run
136
+ </span>
137
+ </div>
138
+
139
+ <textarea
140
+ value={sql}
141
+ onChange={(e) => setSql(e.target.value)}
142
+ onKeyDown={handleKeyDown}
143
+ placeholder="SELECT * FROM entities LIMIT 10"
144
+ className="w-full h-40 font-mono text-sm bg-background border border-input rounded-md px-4 py-3 resize-y focus:outline-none focus:ring-2 focus:ring-ring"
145
+ spellCheck={false}
146
+ />
147
+
148
+ <div className="flex items-center gap-3">
149
+ <Button onClick={handleRun} disabled={loading || !sql.trim()}>
150
+ {loading ? (
151
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
152
+ ) : (
153
+ <Play className="h-4 w-4 mr-2" />
154
+ )}
155
+ Run Query
156
+ </Button>
157
+
158
+ {result && (
159
+ <span className="flex items-center gap-1.5 text-sm text-muted-foreground">
160
+ <Clock className="h-3.5 w-3.5" />
161
+ {result.rowCount} row{result.rowCount !== 1 ? 's' : ''} in{' '}
162
+ {result.duration}ms
163
+ </span>
164
+ )}
165
+ </div>
166
+ </div>
167
+
168
+ {/* Error */}
169
+ {error && !loading && (
170
+ <div className="flex items-start gap-2 text-destructive bg-destructive/10 border border-destructive/20 rounded-md p-4 mb-6">
171
+ <AlertCircle className="h-5 w-5 shrink-0 mt-0.5" />
172
+ <pre className="text-sm whitespace-pre-wrap font-mono">{error}</pre>
173
+ </div>
174
+ )}
175
+
176
+ {/* Results table */}
177
+ {result && result.columns.length > 0 && (
178
+ <div className="bg-card rounded-lg border border-border overflow-hidden">
179
+ <div className="overflow-x-auto">
180
+ <table className="w-full">
181
+ <thead className="bg-muted/50">
182
+ <tr>
183
+ {result.columns.map((col) => (
184
+ <th
185
+ key={col}
186
+ className="px-4 py-3 text-left text-sm font-medium text-muted-foreground border-b border-border whitespace-nowrap"
187
+ >
188
+ {col}
189
+ </th>
190
+ ))}
191
+ </tr>
192
+ </thead>
193
+ <tbody>
194
+ {result.rows.map((row, i) => (
195
+ <tr
196
+ key={i}
197
+ className="border-b border-border hover:bg-muted/50"
198
+ >
199
+ {result.columns.map((col) => (
200
+ <td
201
+ key={col}
202
+ className="px-4 py-3 text-sm font-mono whitespace-nowrap max-w-xs truncate"
203
+ title={formatValue(row[col])}
204
+ >
205
+ <CellValue value={row[col]} />
206
+ </td>
207
+ ))}
208
+ </tr>
209
+ ))}
210
+ </tbody>
211
+ </table>
212
+ </div>
213
+ </div>
214
+ )}
215
+
216
+ {/* Empty result */}
217
+ {result && result.columns.length === 0 && (
218
+ <p className="text-sm text-muted-foreground">
219
+ Query executed successfully but returned no columns.
220
+ </p>
221
+ )}
222
+ </PageContainer>
223
+ )
224
+ }
225
+
226
+ function formatValue(value: unknown): string {
227
+ if (value === null || value === undefined) return 'NULL'
228
+ if (typeof value === 'object') return JSON.stringify(value)
229
+ return String(value)
230
+ }
231
+
232
+ function CellValue({ value }: { value: unknown }) {
233
+ const str = formatValue(value)
234
+ if (typeof value === 'string' && UUID_REGEX.test(value)) {
235
+ return (
236
+ <Link
237
+ to={`/entity/${value}`}
238
+ className="text-primary hover:underline"
239
+ title="Inspect entity"
240
+ >
241
+ {str}
242
+ </Link>
243
+ )
244
+ }
245
+ return <>{str}</>
246
+ }