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,94 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { useParams } from 'react-router-dom'
|
|
3
|
+
import { type ColumnDef } from '@tanstack/react-table'
|
|
4
|
+
import { fetchTableData, deleteTableRecords } from '../lib/api'
|
|
5
|
+
import { PageContainer } from "../components/PageContainer";
|
|
6
|
+
import { PageHeader } from "../components/PageHeader";
|
|
7
|
+
import { SearchBar } from "../components/SearchBar";
|
|
8
|
+
import { DataTable } from '../components/DataTable'
|
|
9
|
+
import { useDataTable } from '../hooks/useDataTable'
|
|
10
|
+
import { createSelectColumn, createTextColumn } from '../utils/columnHelpers'
|
|
11
|
+
|
|
12
|
+
interface TableRecord {
|
|
13
|
+
[key: string]: any
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Table() {
|
|
17
|
+
const { name } = useParams<{ name: string }>()
|
|
18
|
+
const [columns, setColumns] = useState<ColumnDef<TableRecord>[]>([])
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
data,
|
|
22
|
+
loading,
|
|
23
|
+
hasMore,
|
|
24
|
+
search,
|
|
25
|
+
sorting,
|
|
26
|
+
selectedRecords,
|
|
27
|
+
setSearch,
|
|
28
|
+
setSorting,
|
|
29
|
+
setSelectedRecords,
|
|
30
|
+
handleDelete,
|
|
31
|
+
loadMoreRef,
|
|
32
|
+
} = useDataTable<TableRecord>({
|
|
33
|
+
key: name || '',
|
|
34
|
+
fetchData: (params) => fetchTableData(name!, params),
|
|
35
|
+
deleteRecords: (ids) => deleteTableRecords(name!, ids),
|
|
36
|
+
fetchErrorMessage: 'Failed to load table data',
|
|
37
|
+
deleteErrorMessage: 'Failed to delete table records',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Generate columns from the first data record
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const sampleRecord = data[0]
|
|
43
|
+
if (sampleRecord && columns.length === 0) {
|
|
44
|
+
const newColumns: ColumnDef<TableRecord>[] = [
|
|
45
|
+
createSelectColumn<TableRecord>(),
|
|
46
|
+
...Object.keys(sampleRecord).map(key =>
|
|
47
|
+
createTextColumn<TableRecord>(key, key)
|
|
48
|
+
),
|
|
49
|
+
]
|
|
50
|
+
setColumns(newColumns)
|
|
51
|
+
}
|
|
52
|
+
}, [data, columns.length])
|
|
53
|
+
|
|
54
|
+
// Reset columns when table name changes
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
setColumns([])
|
|
57
|
+
}, [name])
|
|
58
|
+
|
|
59
|
+
if (!name) {
|
|
60
|
+
return <div className="p-8">Table name not found</div>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<PageContainer>
|
|
65
|
+
<PageHeader
|
|
66
|
+
title={`${name} Table`}
|
|
67
|
+
description={`Browse and manage records in the ${name} table`}
|
|
68
|
+
/>
|
|
69
|
+
<SearchBar
|
|
70
|
+
search={search}
|
|
71
|
+
onSearchChange={setSearch}
|
|
72
|
+
placeholder="Search records..."
|
|
73
|
+
selectedCount={selectedRecords.size}
|
|
74
|
+
onDelete={handleDelete}
|
|
75
|
+
itemSingular="record"
|
|
76
|
+
itemPlural="records"
|
|
77
|
+
/>
|
|
78
|
+
<DataTable
|
|
79
|
+
data={data}
|
|
80
|
+
columns={columns}
|
|
81
|
+
loading={loading}
|
|
82
|
+
hasMore={hasMore}
|
|
83
|
+
sorting={sorting}
|
|
84
|
+
onSortingChange={setSorting}
|
|
85
|
+
selectedRecords={selectedRecords}
|
|
86
|
+
onSelectionChange={setSelectedRecords}
|
|
87
|
+
getRecordId={(record) => String(record.id)}
|
|
88
|
+
loadMoreRef={loadMoreRef}
|
|
89
|
+
emptyMessage="No records found"
|
|
90
|
+
loadingMessage="Loading more records..."
|
|
91
|
+
/>
|
|
92
|
+
</PageContainer>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate, Link } from 'react-router-dom'
|
|
3
|
+
import {
|
|
4
|
+
Search,
|
|
5
|
+
Loader2,
|
|
6
|
+
AlertCircle,
|
|
7
|
+
Box,
|
|
8
|
+
Layers,
|
|
9
|
+
FlameIcon,
|
|
10
|
+
RefreshCw,
|
|
11
|
+
Trash2,
|
|
12
|
+
} from 'lucide-react'
|
|
13
|
+
import { PageContainer } from '../components/PageContainer'
|
|
14
|
+
import { Input } from '../components/ui/input'
|
|
15
|
+
import { Button } from '../components/ui/button'
|
|
16
|
+
import { fetchStats, type StudioStats } from '../lib/api'
|
|
17
|
+
import { toast } from 'sonner'
|
|
18
|
+
|
|
19
|
+
export function Welcome() {
|
|
20
|
+
const navigate = useNavigate()
|
|
21
|
+
const [stats, setStats] = useState<StudioStats | null>(null)
|
|
22
|
+
const [loading, setLoading] = useState(true)
|
|
23
|
+
const [error, setError] = useState<string | null>(null)
|
|
24
|
+
const [entitySearch, setEntitySearch] = useState('')
|
|
25
|
+
|
|
26
|
+
const loadStats = async () => {
|
|
27
|
+
setLoading(true)
|
|
28
|
+
setError(null)
|
|
29
|
+
try {
|
|
30
|
+
const data = await fetchStats()
|
|
31
|
+
setStats(data)
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch stats'
|
|
34
|
+
setError(message)
|
|
35
|
+
toast.error(message)
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
loadStats()
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
const handleEntityLookup = (e: React.FormEvent) => {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
const trimmed = entitySearch.trim()
|
|
48
|
+
if (!trimmed) return
|
|
49
|
+
navigate(`/entity/${trimmed}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<PageContainer>
|
|
54
|
+
<div className="max-w-5xl">
|
|
55
|
+
{/* Header */}
|
|
56
|
+
<div className="flex items-center justify-between mb-8">
|
|
57
|
+
<div>
|
|
58
|
+
<h1 className="text-3xl font-bold text-primary">Dashboard</h1>
|
|
59
|
+
<p className="text-muted-foreground mt-1">
|
|
60
|
+
Overview of your ECS database
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
<Button
|
|
64
|
+
variant="outline"
|
|
65
|
+
size="sm"
|
|
66
|
+
onClick={loadStats}
|
|
67
|
+
disabled={loading}
|
|
68
|
+
>
|
|
69
|
+
{loading ? (
|
|
70
|
+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
71
|
+
) : (
|
|
72
|
+
<RefreshCw className="h-4 w-4 mr-2" />
|
|
73
|
+
)}
|
|
74
|
+
Refresh
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Quick entity lookup */}
|
|
79
|
+
<form onSubmit={handleEntityLookup} className="flex gap-2 mb-8 max-w-lg">
|
|
80
|
+
<div className="relative flex-1">
|
|
81
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
82
|
+
<Input
|
|
83
|
+
value={entitySearch}
|
|
84
|
+
onChange={(e) => setEntitySearch(e.target.value)}
|
|
85
|
+
placeholder="Quick entity lookup — paste UUID..."
|
|
86
|
+
className="pl-9 font-mono text-sm"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<Button type="submit" disabled={!entitySearch.trim()}>
|
|
90
|
+
Inspect
|
|
91
|
+
</Button>
|
|
92
|
+
</form>
|
|
93
|
+
|
|
94
|
+
{/* Error */}
|
|
95
|
+
{error && !loading && (
|
|
96
|
+
<div className="flex items-center gap-2 text-destructive bg-destructive/10 border border-destructive/20 rounded-md p-4 mb-6">
|
|
97
|
+
<AlertCircle className="h-5 w-5 shrink-0" />
|
|
98
|
+
<span>{error}</span>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{/* Loading skeleton */}
|
|
103
|
+
{loading && !stats && (
|
|
104
|
+
<div className="flex items-center gap-2 text-muted-foreground py-12 justify-center">
|
|
105
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
106
|
+
<span>Loading stats...</span>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{/* Stats content */}
|
|
111
|
+
{stats && (
|
|
112
|
+
<>
|
|
113
|
+
{/* Summary cards */}
|
|
114
|
+
<div className="grid gap-4 md:grid-cols-3 mb-8">
|
|
115
|
+
<SummaryCard
|
|
116
|
+
icon={Box}
|
|
117
|
+
label="Entities"
|
|
118
|
+
value={stats.entities.active}
|
|
119
|
+
sub={
|
|
120
|
+
stats.entities.deleted > 0
|
|
121
|
+
? `${stats.entities.deleted} deleted`
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
<SummaryCard
|
|
126
|
+
icon={Layers}
|
|
127
|
+
label="Component Types"
|
|
128
|
+
value={stats.componentTypes.length}
|
|
129
|
+
/>
|
|
130
|
+
<SummaryCard
|
|
131
|
+
icon={FlameIcon}
|
|
132
|
+
label="Archetypes"
|
|
133
|
+
value={stats.archetypes.length}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Two-column layout */}
|
|
138
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
139
|
+
{/* Archetypes */}
|
|
140
|
+
<div className="border border-border rounded-lg bg-card">
|
|
141
|
+
<div className="px-5 py-4 border-b border-border">
|
|
142
|
+
<h2 className="font-semibold">Archetypes</h2>
|
|
143
|
+
</div>
|
|
144
|
+
{stats.archetypes.length === 0 ? (
|
|
145
|
+
<p className="p-5 text-sm text-muted-foreground">
|
|
146
|
+
No archetypes registered.
|
|
147
|
+
</p>
|
|
148
|
+
) : (
|
|
149
|
+
<div className="divide-y divide-border">
|
|
150
|
+
{stats.archetypes.map((a) => (
|
|
151
|
+
<Link
|
|
152
|
+
key={a.name}
|
|
153
|
+
to={`/archetype/${a.name}`}
|
|
154
|
+
className="flex items-center justify-between px-5 py-3 hover:bg-accent/50 transition-colors"
|
|
155
|
+
>
|
|
156
|
+
<div>
|
|
157
|
+
<span className="text-sm font-medium">{a.name}</span>
|
|
158
|
+
<span className="text-xs text-muted-foreground ml-2">
|
|
159
|
+
{a.componentCount} component{a.componentCount !== 1 ? 's' : ''}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
<span className="text-sm font-mono tabular-nums">
|
|
163
|
+
{a.entityCount.toLocaleString()}
|
|
164
|
+
</span>
|
|
165
|
+
</Link>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Component types */}
|
|
172
|
+
<div className="border border-border rounded-lg bg-card">
|
|
173
|
+
<div className="px-5 py-4 border-b border-border">
|
|
174
|
+
<h2 className="font-semibold">Component Types</h2>
|
|
175
|
+
</div>
|
|
176
|
+
{stats.componentTypes.length === 0 ? (
|
|
177
|
+
<p className="p-5 text-sm text-muted-foreground">
|
|
178
|
+
No components found.
|
|
179
|
+
</p>
|
|
180
|
+
) : (
|
|
181
|
+
<div className="divide-y divide-border max-h-96 overflow-auto">
|
|
182
|
+
{stats.componentTypes.map((ct) => (
|
|
183
|
+
<div
|
|
184
|
+
key={ct.name}
|
|
185
|
+
className="flex items-center justify-between px-5 py-3"
|
|
186
|
+
>
|
|
187
|
+
<span className="text-sm">{ct.name}</span>
|
|
188
|
+
<span className="text-sm font-mono tabular-nums text-muted-foreground">
|
|
189
|
+
{ct.count.toLocaleString()}
|
|
190
|
+
</span>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Deleted entities note */}
|
|
199
|
+
{stats.entities.deleted > 0 && (
|
|
200
|
+
<div className="mt-6 flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 rounded-md px-4 py-3">
|
|
201
|
+
<Trash2 className="h-4 w-4 shrink-0" />
|
|
202
|
+
<span>
|
|
203
|
+
{stats.entities.deleted.toLocaleString()} soft-deleted{' '}
|
|
204
|
+
{stats.entities.deleted === 1 ? 'entity' : 'entities'} in the
|
|
205
|
+
database ({stats.entities.total.toLocaleString()} total)
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</PageContainer>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function SummaryCard({
|
|
217
|
+
icon: Icon,
|
|
218
|
+
label,
|
|
219
|
+
value,
|
|
220
|
+
sub,
|
|
221
|
+
}: {
|
|
222
|
+
icon: typeof Box
|
|
223
|
+
label: string
|
|
224
|
+
value: number
|
|
225
|
+
sub?: string
|
|
226
|
+
}) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="border border-border rounded-lg p-5 bg-card">
|
|
229
|
+
<div className="flex items-center gap-3 mb-3">
|
|
230
|
+
<div className="p-2 rounded-md bg-primary/10">
|
|
231
|
+
<Icon className="h-5 w-5 text-primary" />
|
|
232
|
+
</div>
|
|
233
|
+
<span className="text-sm text-muted-foreground">{label}</span>
|
|
234
|
+
</div>
|
|
235
|
+
<p className="text-3xl font-bold tabular-nums">{value.toLocaleString()}</p>
|
|
236
|
+
{sub && (
|
|
237
|
+
<p className="text-xs text-muted-foreground mt-1">{sub}</p>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RouteObject } from 'react-router-dom'
|
|
2
|
+
import { Layout } from './components/Layout'
|
|
3
|
+
import { Welcome } from './pages/Welcome'
|
|
4
|
+
import { ArcheType } from './pages/ArcheType'
|
|
5
|
+
import { Table } from './pages/Table'
|
|
6
|
+
import { EntityInspector } from './pages/EntityInspector'
|
|
7
|
+
import { Components } from './pages/Components'
|
|
8
|
+
import { QueryRunner } from './pages/QueryRunner'
|
|
9
|
+
|
|
10
|
+
export const routes: RouteObject[] = [
|
|
11
|
+
{
|
|
12
|
+
path: '/',
|
|
13
|
+
element: <Layout />,
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
index: true,
|
|
17
|
+
element: <Welcome />,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
path: 'archetype/:name',
|
|
21
|
+
element: <ArcheType />,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: 'table/:name',
|
|
25
|
+
element: <Table />,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
path: 'entity/:id',
|
|
29
|
+
element: <EntityInspector />,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: 'entity',
|
|
33
|
+
element: <EntityInspector />,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: 'components',
|
|
37
|
+
element: <Components />,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
path: 'query',
|
|
41
|
+
element: <QueryRunner />,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { persist } from 'zustand/middleware'
|
|
3
|
+
|
|
4
|
+
interface ArcheTypeSettingsState {
|
|
5
|
+
useRealDbFieldName: boolean
|
|
6
|
+
autoExpandRow: boolean
|
|
7
|
+
showDeleted: boolean
|
|
8
|
+
|
|
9
|
+
// Actions
|
|
10
|
+
setUseRealDbFieldName: (value: boolean) => void
|
|
11
|
+
setAutoExpandRow: (value: boolean) => void
|
|
12
|
+
setShowDeleted: (value: boolean) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useArcheTypeSettings = create<ArcheTypeSettingsState>()(
|
|
16
|
+
persist(
|
|
17
|
+
(set) => ({
|
|
18
|
+
useRealDbFieldName: false,
|
|
19
|
+
autoExpandRow: true,
|
|
20
|
+
showDeleted: false,
|
|
21
|
+
|
|
22
|
+
setUseRealDbFieldName: (value) => set({ useRealDbFieldName: value }),
|
|
23
|
+
setAutoExpandRow: (value) => set({ autoExpandRow: value }),
|
|
24
|
+
setShowDeleted: (value) => set({ showDeleted: value }),
|
|
25
|
+
}),
|
|
26
|
+
{
|
|
27
|
+
name: 'bunsane-archetype-settings',
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { persist } from "zustand/middleware";
|
|
3
|
+
|
|
4
|
+
export interface Metadata {
|
|
5
|
+
archeTypes: Record<string, {
|
|
6
|
+
fieldName: string
|
|
7
|
+
componentName: string
|
|
8
|
+
fieldLabel: string
|
|
9
|
+
nullable?: boolean
|
|
10
|
+
}[]>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StudioState {
|
|
14
|
+
metadata: Metadata | null;
|
|
15
|
+
tables: string[];
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
isSidebarCollapsed: boolean;
|
|
19
|
+
expandedSections: Record<string, boolean>;
|
|
20
|
+
|
|
21
|
+
// Actions
|
|
22
|
+
setMetadata: (metadata: Metadata) => void;
|
|
23
|
+
setTables: (tables: string[]) => void;
|
|
24
|
+
setLoading: (loading: boolean) => void;
|
|
25
|
+
setError: (error: string | null) => void;
|
|
26
|
+
setSidebarCollapsed: (collapsed: boolean) => void;
|
|
27
|
+
toggleSection: (section: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const useStudioStore = create<StudioState>()(
|
|
31
|
+
persist(
|
|
32
|
+
(set) => ({
|
|
33
|
+
metadata: null,
|
|
34
|
+
tables: [],
|
|
35
|
+
isLoading: false,
|
|
36
|
+
error: null,
|
|
37
|
+
isSidebarCollapsed: false,
|
|
38
|
+
expandedSections: {
|
|
39
|
+
archeTypes: true,
|
|
40
|
+
tables: true,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
setMetadata: (metadata) => set({ metadata }),
|
|
44
|
+
setTables: (tables) => set({ tables }),
|
|
45
|
+
setLoading: (loading) => set({ isLoading: loading }),
|
|
46
|
+
setError: (error) => set({ error }),
|
|
47
|
+
setSidebarCollapsed: (collapsed) =>
|
|
48
|
+
set({ isSidebarCollapsed: collapsed }),
|
|
49
|
+
toggleSection: (section) =>
|
|
50
|
+
set((state) => ({
|
|
51
|
+
expandedSections: {
|
|
52
|
+
...state.expandedSections,
|
|
53
|
+
[section]: !state.expandedSections[section],
|
|
54
|
+
},
|
|
55
|
+
})),
|
|
56
|
+
}),
|
|
57
|
+
{
|
|
58
|
+
name: "bunsane-studio-storage",
|
|
59
|
+
partialize: (state) => ({
|
|
60
|
+
isSidebarCollapsed: state.isSidebarCollapsed,
|
|
61
|
+
expandedSections: state.expandedSections,
|
|
62
|
+
}),
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type ColumnDef } from '@tanstack/react-table'
|
|
2
|
+
import { Link } from 'react-router-dom'
|
|
3
|
+
import ReactJson from 'react-json-view'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates the select checkbox column for data tables
|
|
7
|
+
*/
|
|
8
|
+
export function createSelectColumn<T>(): ColumnDef<T> {
|
|
9
|
+
return {
|
|
10
|
+
id: 'select',
|
|
11
|
+
header: ({ table }) => (
|
|
12
|
+
<input
|
|
13
|
+
type="checkbox"
|
|
14
|
+
checked={table.getIsAllRowsSelected()}
|
|
15
|
+
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
16
|
+
className="rounded border-border"
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
cell: ({ row }) => (
|
|
20
|
+
<input
|
|
21
|
+
type="checkbox"
|
|
22
|
+
checked={row.getIsSelected()}
|
|
23
|
+
onChange={row.getToggleSelectedHandler()}
|
|
24
|
+
className="rounded border-border"
|
|
25
|
+
/>
|
|
26
|
+
),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Renders a cell value, displaying objects as ReactJson and primitives as text
|
|
32
|
+
*/
|
|
33
|
+
export function renderCellValue(
|
|
34
|
+
value: any,
|
|
35
|
+
extractValue = false,
|
|
36
|
+
autoExpandRow = false
|
|
37
|
+
): JSX.Element {
|
|
38
|
+
// If extractValue is true and value has a .value property, extract it
|
|
39
|
+
let actualValue =
|
|
40
|
+
extractValue && value?.value !== undefined ? value.value : value;
|
|
41
|
+
|
|
42
|
+
actualValue = actualValue ?? "-";
|
|
43
|
+
|
|
44
|
+
if (typeof actualValue === "object" && actualValue !== null) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="max-w-xs">
|
|
47
|
+
<ReactJson
|
|
48
|
+
src={actualValue}
|
|
49
|
+
collapsed={autoExpandRow ? 2 : 1}
|
|
50
|
+
enableClipboard
|
|
51
|
+
displayDataTypes={false}
|
|
52
|
+
displayObjectSize={false}
|
|
53
|
+
name={null}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<span className="truncate max-w-xs block">{String(actualValue)}</span>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a standard text column with proper rendering
|
|
66
|
+
*/
|
|
67
|
+
export function createTextColumn<T>(
|
|
68
|
+
key: string,
|
|
69
|
+
header: string,
|
|
70
|
+
options: {
|
|
71
|
+
extractValue?: boolean;
|
|
72
|
+
className?: string;
|
|
73
|
+
autoExpandRow?: boolean;
|
|
74
|
+
} = {}
|
|
75
|
+
): ColumnDef<T> {
|
|
76
|
+
return {
|
|
77
|
+
accessorKey: key,
|
|
78
|
+
header,
|
|
79
|
+
cell: ({ getValue }) => {
|
|
80
|
+
const value = getValue();
|
|
81
|
+
return renderCellValue(
|
|
82
|
+
value,
|
|
83
|
+
options.extractValue,
|
|
84
|
+
options.autoExpandRow
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates an ID column with monospace font styling and link to Entity Inspector
|
|
92
|
+
*/
|
|
93
|
+
export function createIdColumn<T>(options?: { linkToEntity?: boolean }): ColumnDef<T> {
|
|
94
|
+
const linkToEntity = options?.linkToEntity ?? false
|
|
95
|
+
return {
|
|
96
|
+
accessorKey: 'id',
|
|
97
|
+
header: 'ID',
|
|
98
|
+
cell: ({ getValue }) => {
|
|
99
|
+
const value = getValue() as string
|
|
100
|
+
if (linkToEntity) {
|
|
101
|
+
return (
|
|
102
|
+
<Link
|
|
103
|
+
to={`/entity/${value}`}
|
|
104
|
+
className="font-mono text-xs text-primary hover:underline"
|
|
105
|
+
title="Inspect entity"
|
|
106
|
+
>
|
|
107
|
+
{value}
|
|
108
|
+
</Link>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
return <span className="font-mono text-xs">{value}</span>
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: '**'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Bunsane studio
|
|
6
|
+
|
|
7
|
+
## About the project
|
|
8
|
+
|
|
9
|
+
the project is to create PostgreSQL database management
|
|
10
|
+
with support for ECS (Entity Component System) model
|
|
11
|
+
|
|
12
|
+
there are some tables that we need to pay attention, i.e:
|
|
13
|
+
- components (ECS table)
|
|
14
|
+
- entities (ECS table)
|
|
15
|
+
- entity_components (ECS table)
|
|
16
|
+
- spatial_ref_sys (postgis table)
|
|
17
|
+
|
|
18
|
+
the tables displays will be devided by 3 part, i.e general table, ECS table, invisible table
|
|
19
|
+
|
|
20
|
+
### General table
|
|
21
|
+
|
|
22
|
+
user can do normal CRUD management to the table, just like normal database table.
|
|
23
|
+
|
|
24
|
+
### ECS table
|
|
25
|
+
|
|
26
|
+
user can do CRUD, but not directly to the table. because the ECS table means to be used to work with ECS thing, it will contain multiple entities.
|
|
27
|
+
Those entities is what user going to CRUD to.
|
|
28
|
+
therefore lets use word table for general table, and entity for ECS table.
|
|
29
|
+
|
|
30
|
+
ECS system design explanation:
|
|
31
|
+
- Entity can be anything e.g user, payment, item, .etc (it can be called as table in traditional design system) it also can be called ArcheType
|
|
32
|
+
- fields that usually used in normal system e.g email, phone (for user) or price (for item) .etc in ECS is a Component
|
|
33
|
+
- there is 3 table dedicated for ECS i.e entities, components, and entity_components (intermediate table for entities and components)
|
|
34
|
+
- entities table contain all entities that exist in the system. the table contain id, created_at, updated_at, and deleted_at
|
|
35
|
+
- components table contain the all useful data that user consume, the table contain id, entity_id, type_id, name, data, created_at, updated_at, and deleted_at (data is jsonb that contain the actual useful data)
|
|
36
|
+
- entity_components is intermediate table. the table contain, entity_id, type_id, component_id, created_at, updated_at, and deleted_at
|
|
37
|
+
|
|
38
|
+
## Feature
|
|
39
|
+
- for now I only need Read feature, but in the future all CRUD should be supported so we can show the edit/delete ui but can disable it for now
|
|
40
|
+
- General table and ECS table should be easily distinguished
|
|
41
|
+
- user can create connection to initialize the database connection but there is default value that point to local postgres with default db
|
|
42
|
+
- the connection detail is stored in localstorage after created
|
|
43
|
+
- user can see the list of tables and entities in the sidebar
|
|
44
|
+
- user can click the table or entity to see the data in the main content area
|
|
45
|
+
- user can see the table column info (name, type, is_nullable, default value) for General table
|
|
46
|
+
- the entity displayed as table, with the columns are id and component types (name from components table), the value for id is from entities.id, and the value for component types is from components.data (jsonb) field
|
|
47
|
+
- the default 50 records can be displayed, then there will be load more button to load 50 more records
|
|
48
|
+
- user can see the total row count of the table
|
|
49
|
+
|
|
50
|
+
## Tech stack
|
|
51
|
+
- Bun for runtime and package manager
|
|
52
|
+
- Vite + React router + @react-router/fs-routes + Tailwind + Shadcn
|
|
53
|
+
- tanstack table
|
|
54
|
+
- zustand for state management
|
|
55
|
+
- sonner for snack/notification
|
|
56
|
+
- lucide-react for icon
|
|
57
|
+
- zod for schema validation
|
|
58
|
+
- react-json-view for json data display
|
|
59
|
+
|
|
60
|
+
## File structure
|
|
61
|
+
- use @react-router/fs-routes see https://reactrouter.com/how-to/file-route-conventions#basic-routes
|
|
62
|
+
- components for ui components
|
|
63
|
+
- store for zustand store
|
|
64
|
+
- ensure its easy to manage and understand
|
|
65
|
+
|
|
66
|
+
## Api endpoint
|
|
67
|
+
- can use /studio/api/tables to get all tables
|
|
68
|
+
|
|
69
|
+
## Code style
|
|
70
|
+
- ensure typesafety with typescript and drizzle orm
|
|
71
|
+
- ensure code is clean and easy to understand
|
|
72
|
+
- ensure proper error handling
|
|
73
|
+
- ensure proper separation of concerns
|
|
74
|
+
|
|
75
|
+
## App UI style
|
|
76
|
+
- for now, only support light theme
|
|
77
|
+
- use shadcn available components/template if available
|
|
78
|
+
- the theme is orange-ish colour like fire in the sky
|
|
79
|
+
- ensure the ui is clean and easy to use
|
|
80
|
+
- ensure hover, and focus styles are properly implemented
|
|
81
|
+
- only support desktop view for now (min width 1024px)
|