bunsane 0.3.2 → 0.5.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 (220) hide show
  1. package/CHANGELOG.md +471 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +93 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +8 -7
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +25 -10
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +383 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/getCacheManager.ts +10 -0
  30. package/core/entity/pendingOps.ts +72 -0
  31. package/core/entity/saveEntity.ts +375 -0
  32. package/core/health.ts +93 -4
  33. package/core/hooks/dispatcher.ts +439 -0
  34. package/core/hooks/guards.ts +155 -0
  35. package/core/hooks/registry.ts +247 -0
  36. package/core/metadata/definitions/Component.ts +1 -1
  37. package/core/metadata/index.ts +15 -4
  38. package/core/middleware/RateLimit.ts +102 -105
  39. package/core/middleware/RequestId.ts +2 -9
  40. package/core/middleware/SecurityHeaders.ts +2 -11
  41. package/core/middleware/headers.ts +28 -0
  42. package/core/remote/OutboxWorker.ts +213 -183
  43. package/core/remote/RemoteManager.ts +401 -400
  44. package/core/remote/StreamConsumer.ts +535 -535
  45. package/core/remote/types.ts +153 -151
  46. package/core/requestScope.ts +34 -0
  47. package/core/scheduler/cronEvaluator.ts +174 -0
  48. package/core/scheduler/lifecycleHooks.ts +21 -0
  49. package/core/scheduler/lockCoordinator.ts +27 -0
  50. package/core/scheduler/metrics.ts +14 -0
  51. package/core/scheduler/taskRunner.ts +420 -0
  52. package/core/validateEnv.ts +10 -0
  53. package/database/DatabaseHelper.ts +128 -101
  54. package/database/IndexingStrategy.ts +72 -2
  55. package/database/PreparedStatementCache.ts +8 -2
  56. package/database/cancellable.ts +35 -22
  57. package/database/index.ts +29 -3
  58. package/database/instrumentedDb.ts +141 -141
  59. package/database/sqlHelpers.ts +3 -1
  60. package/endpoints/archetypes.ts +2 -8
  61. package/endpoints/tables.ts +6 -1
  62. package/gql/index.ts +1 -1
  63. package/gql/schema/index.ts +15 -4
  64. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  65. package/package.json +22 -1
  66. package/query/CTENode.ts +5 -3
  67. package/query/ComponentInclusionNode.ts +245 -14
  68. package/query/OrNode.ts +8 -19
  69. package/query/Query.ts +208 -79
  70. package/query/QueryContext.ts +6 -0
  71. package/query/QueryDAG.ts +7 -2
  72. package/query/membershipSource.ts +66 -0
  73. package/storage/LocalStorageProvider.ts +8 -3
  74. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  75. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  76. package/studio/{index.html → dist/index.html} +3 -2
  77. package/swagger/generator.ts +11 -1
  78. package/upload/UploadManager.ts +8 -6
  79. package/utils/uuid.ts +40 -10
  80. package/.claude/scheduled_tasks.lock +0 -1
  81. package/.claude/settings.local.json +0 -47
  82. package/.prettierrc +0 -4
  83. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  84. package/.serena/memories/architecture.md +0 -154
  85. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  86. package/.serena/memories/code_style_and_conventions.md +0 -76
  87. package/.serena/memories/project_overview.md +0 -43
  88. package/.serena/memories/schema-dsl-plan.md +0 -107
  89. package/.serena/memories/suggested_commands.md +0 -80
  90. package/.serena/memories/typescript-compilation-status.md +0 -54
  91. package/.serena/project.yml +0 -114
  92. package/BunSane.jpg +0 -0
  93. package/CLAUDE.md +0 -198
  94. package/TODO.md +0 -2
  95. package/bun.lock +0 -302
  96. package/bunfig.toml +0 -10
  97. package/docs/RFC_APP_REFACTOR.md +0 -248
  98. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  99. package/docs/SCALABILITY_PLAN.md +0 -175
  100. package/studio/bun.lock +0 -482
  101. package/studio/package.json +0 -39
  102. package/studio/postcss.config.js +0 -6
  103. package/studio/src/components/DataTable.tsx +0 -211
  104. package/studio/src/components/Layout.tsx +0 -13
  105. package/studio/src/components/PageContainer.tsx +0 -9
  106. package/studio/src/components/PageHeader.tsx +0 -13
  107. package/studio/src/components/SearchBar.tsx +0 -57
  108. package/studio/src/components/Sidebar.tsx +0 -294
  109. package/studio/src/components/ui/button.tsx +0 -56
  110. package/studio/src/components/ui/checkbox.tsx +0 -26
  111. package/studio/src/components/ui/input.tsx +0 -25
  112. package/studio/src/hooks/useDataTable.ts +0 -131
  113. package/studio/src/index.css +0 -36
  114. package/studio/src/lib/api.ts +0 -186
  115. package/studio/src/lib/utils.ts +0 -13
  116. package/studio/src/main.tsx +0 -17
  117. package/studio/src/pages/ArcheType.tsx +0 -239
  118. package/studio/src/pages/Components.tsx +0 -124
  119. package/studio/src/pages/EntityInspector.tsx +0 -302
  120. package/studio/src/pages/QueryRunner.tsx +0 -246
  121. package/studio/src/pages/Table.tsx +0 -94
  122. package/studio/src/pages/Welcome.tsx +0 -241
  123. package/studio/src/routes.tsx +0 -45
  124. package/studio/src/store/archeTypeSettings.ts +0 -30
  125. package/studio/src/store/studio.ts +0 -65
  126. package/studio/src/utils/columnHelpers.tsx +0 -114
  127. package/studio/studio-instructions.md +0 -81
  128. package/studio/tailwind.config.js +0 -77
  129. package/studio/utils.ts +0 -54
  130. package/studio/vite.config.js +0 -19
  131. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  132. package/tests/benchmark/bunfig.toml +0 -9
  133. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  134. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  135. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  136. package/tests/benchmark/fixtures/index.ts +0 -6
  137. package/tests/benchmark/index.ts +0 -22
  138. package/tests/benchmark/noop-preload.ts +0 -3
  139. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  140. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  141. package/tests/benchmark/runners/index.ts +0 -4
  142. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  143. package/tests/benchmark/scripts/generate-db.ts +0 -344
  144. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  145. package/tests/e2e/http.test.ts +0 -130
  146. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  147. package/tests/fixtures/components/TestOrder.ts +0 -23
  148. package/tests/fixtures/components/TestProduct.ts +0 -23
  149. package/tests/fixtures/components/TestUser.ts +0 -20
  150. package/tests/fixtures/components/index.ts +0 -6
  151. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  152. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  153. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  154. package/tests/helpers/MockRedisClient.ts +0 -113
  155. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  156. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  157. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  158. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  159. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  160. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  161. package/tests/integration/query/Query.abort.test.ts +0 -66
  162. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  163. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  164. package/tests/integration/query/Query.exec.test.ts +0 -576
  165. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  166. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  167. package/tests/integration/remote/dlq.test.ts +0 -175
  168. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  169. package/tests/integration/remote/outbox.test.ts +0 -130
  170. package/tests/integration/remote/rpc.test.ts +0 -177
  171. package/tests/pglite-setup.ts +0 -62
  172. package/tests/setup.ts +0 -164
  173. package/tests/stress/BenchmarkRunner.ts +0 -203
  174. package/tests/stress/DataSeeder.ts +0 -190
  175. package/tests/stress/StressTestReporter.ts +0 -229
  176. package/tests/stress/cursor-perf-test.ts +0 -171
  177. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  178. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  179. package/tests/stress/index.ts +0 -7
  180. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  181. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  182. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  183. package/tests/unit/BatchLoader.test.ts +0 -196
  184. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  185. package/tests/unit/cache/CacheManager.test.ts +0 -498
  186. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  187. package/tests/unit/cache/RedisCache.test.ts +0 -411
  188. package/tests/unit/database/cancellable.test.ts +0 -81
  189. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  190. package/tests/unit/entity/Entity.components.test.ts +0 -317
  191. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  192. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  193. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  194. package/tests/unit/entity/Entity.test.ts +0 -345
  195. package/tests/unit/gql/depthLimit.test.ts +0 -203
  196. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  197. package/tests/unit/health/Health.test.ts +0 -129
  198. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  199. package/tests/unit/middleware/Middleware.test.ts +0 -98
  200. package/tests/unit/middleware/RequestId.test.ts +0 -54
  201. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  202. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  203. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  204. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  205. package/tests/unit/query/Query.test.ts +0 -310
  206. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  207. package/tests/unit/remote/RemoteError.test.ts +0 -55
  208. package/tests/unit/remote/decorators.test.ts +0 -195
  209. package/tests/unit/remote/metrics.test.ts +0 -115
  210. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  211. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  212. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  213. package/tests/unit/schema/schema-integration.test.ts +0 -426
  214. package/tests/unit/schema/schema.test.ts +0 -580
  215. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  216. package/tests/unit/upload/RestUpload.test.ts +0 -267
  217. package/tests/unit/validateEnv.test.ts +0 -82
  218. package/tests/utils/entity-tracker.ts +0 -57
  219. package/tests/utils/index.ts +0 -13
  220. package/tests/utils/test-context.ts +0 -149
@@ -1,124 +0,0 @@
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
- }
@@ -1,302 +0,0 @@
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
- }
@@ -1,246 +0,0 @@
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
- }