bunsane 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
|
@@ -0,0 +1,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
|
+
}
|