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