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,25 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
|
|
5
|
+
export interface InputProps
|
|
6
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
7
|
+
|
|
8
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
9
|
+
({ className, type, ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<input
|
|
12
|
+
type={type}
|
|
13
|
+
className={cn(
|
|
14
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
ref={ref}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
Input.displayName = "Input"
|
|
24
|
+
|
|
25
|
+
export { Input }
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { useInView } from 'react-intersection-observer'
|
|
3
|
+
import { type SortingState } from '@tanstack/react-table'
|
|
4
|
+
import { toast } from 'sonner'
|
|
5
|
+
import { pluralize } from '../lib/utils'
|
|
6
|
+
|
|
7
|
+
interface UseDataTableOptions<T> {
|
|
8
|
+
/** Unique identifier for the data (e.g., table name, archetype name) */
|
|
9
|
+
key: string
|
|
10
|
+
/** Function to fetch data */
|
|
11
|
+
fetchData: (params: { offset: number; limit: number; search?: string }) => Promise<{
|
|
12
|
+
data: T[]
|
|
13
|
+
hasMore: boolean
|
|
14
|
+
total?: number
|
|
15
|
+
}>
|
|
16
|
+
/** Function to delete records by IDs */
|
|
17
|
+
deleteRecords: (ids: string[]) => Promise<void>
|
|
18
|
+
/** Optional: Custom error message for fetch failure */
|
|
19
|
+
fetchErrorMessage?: string
|
|
20
|
+
/** Optional: Custom error message for delete failure */
|
|
21
|
+
deleteErrorMessage?: string
|
|
22
|
+
/** Optional: Custom success message for delete (use {count} and {item} as placeholders) */
|
|
23
|
+
deleteSuccessMessage?: string
|
|
24
|
+
/** Optional: Singular form of the item type (e.g., "record", "entity") */
|
|
25
|
+
itemSingular?: string
|
|
26
|
+
/** Optional: Plural form of the item type (e.g., "records", "entities") */
|
|
27
|
+
itemPlural?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useDataTable<T>({
|
|
31
|
+
key,
|
|
32
|
+
fetchData,
|
|
33
|
+
deleteRecords,
|
|
34
|
+
fetchErrorMessage = 'Failed to load data',
|
|
35
|
+
deleteErrorMessage = 'Failed to delete records',
|
|
36
|
+
deleteSuccessMessage = 'Deleted {count} {item}',
|
|
37
|
+
itemSingular = 'record',
|
|
38
|
+
itemPlural = 'records',
|
|
39
|
+
}: UseDataTableOptions<T>) {
|
|
40
|
+
const [data, setData] = useState<T[]>([])
|
|
41
|
+
const [loading, setLoading] = useState(false)
|
|
42
|
+
const [hasMore, setHasMore] = useState(true)
|
|
43
|
+
const [total, setTotal] = useState<number | null>(null)
|
|
44
|
+
const [search, setSearch] = useState('')
|
|
45
|
+
const [sorting, setSorting] = useState<SortingState>([])
|
|
46
|
+
const [selectedRecords, setSelectedRecords] = useState<Set<string>>(new Set())
|
|
47
|
+
const { ref, inView } = useInView()
|
|
48
|
+
|
|
49
|
+
const loadMore = async (reset = false) => {
|
|
50
|
+
if (loading || (!hasMore && !reset)) return
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
setLoading(true)
|
|
54
|
+
const offset = reset ? 0 : data.length
|
|
55
|
+
const result = await fetchData({
|
|
56
|
+
offset,
|
|
57
|
+
limit: 50,
|
|
58
|
+
search: search || undefined,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (reset) {
|
|
62
|
+
setData(result.data)
|
|
63
|
+
} else {
|
|
64
|
+
setData(prev => [...prev, ...result.data])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (result.total !== undefined) {
|
|
68
|
+
setTotal(result.total)
|
|
69
|
+
}
|
|
70
|
+
setHasMore(result.hasMore)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
toast.error(fetchErrorMessage)
|
|
73
|
+
} finally {
|
|
74
|
+
setLoading(false)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reset and load data when key or search changes
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (key) {
|
|
81
|
+
setData([])
|
|
82
|
+
setHasMore(true)
|
|
83
|
+
setSelectedRecords(new Set())
|
|
84
|
+
loadMore(true)
|
|
85
|
+
}
|
|
86
|
+
}, [key, search])
|
|
87
|
+
|
|
88
|
+
// Infinite scroll: load more when scroll observer is in view
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (inView && hasMore && !loading) {
|
|
91
|
+
loadMore()
|
|
92
|
+
}
|
|
93
|
+
}, [inView, hasMore, loading])
|
|
94
|
+
|
|
95
|
+
const handleDelete = async () => {
|
|
96
|
+
if (selectedRecords.size === 0) return
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await deleteRecords(Array.from(selectedRecords))
|
|
100
|
+
const itemText = pluralize(selectedRecords.size, itemSingular, itemPlural)
|
|
101
|
+
toast.success(deleteSuccessMessage.replace('{count}', selectedRecords.size.toString()).replace('{item}', itemText))
|
|
102
|
+
setSelectedRecords(new Set())
|
|
103
|
+
loadMore(true)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
toast.error(deleteErrorMessage)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
// State
|
|
111
|
+
data,
|
|
112
|
+
loading,
|
|
113
|
+
hasMore,
|
|
114
|
+
total,
|
|
115
|
+
search,
|
|
116
|
+
sorting,
|
|
117
|
+
selectedRecords,
|
|
118
|
+
|
|
119
|
+
// Setters
|
|
120
|
+
setSearch,
|
|
121
|
+
setSorting,
|
|
122
|
+
setSelectedRecords,
|
|
123
|
+
setData,
|
|
124
|
+
|
|
125
|
+
// Handlers
|
|
126
|
+
handleDelete,
|
|
127
|
+
|
|
128
|
+
// Refs
|
|
129
|
+
loadMoreRef: ref,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 222.2 84% 4.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
13
|
+
--primary: 24 95% 53%;
|
|
14
|
+
--primary-foreground: 0 0% 98%;
|
|
15
|
+
--secondary: 210 40% 96%;
|
|
16
|
+
--secondary-foreground: 222.2 84% 4.9%;
|
|
17
|
+
--muted: 210 40% 96%;
|
|
18
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
19
|
+
--accent: 210 40% 96%;
|
|
20
|
+
--accent-foreground: 222.2 84% 4.9%;
|
|
21
|
+
--destructive: 0 84.2% 60.2%;
|
|
22
|
+
--destructive-foreground: 210 40% 98%;
|
|
23
|
+
--border: 214.3 31.8% 91.4%;
|
|
24
|
+
--input: 214.3 31.8% 91.4%;
|
|
25
|
+
--ring: 24 95% 53%;
|
|
26
|
+
--radius: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
* {
|
|
30
|
+
@apply border-border;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
@apply bg-background text-foreground;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const API_BASE = '/studio/api'
|
|
2
|
+
|
|
3
|
+
export interface TableData {
|
|
4
|
+
data: Record<string, unknown>[]
|
|
5
|
+
hasMore: boolean
|
|
6
|
+
total?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ArcheTypeData {
|
|
10
|
+
data: Record<string, unknown>[]
|
|
11
|
+
hasMore: boolean
|
|
12
|
+
total?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TablesResponse {
|
|
16
|
+
tables: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetchTables(): Promise<string[]> {
|
|
20
|
+
const response = await fetch(`${API_BASE}/tables`)
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error('Failed to fetch tables')
|
|
23
|
+
}
|
|
24
|
+
const data: TablesResponse = await response.json()
|
|
25
|
+
return data.tables
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fetchTableData(
|
|
29
|
+
tableName: string,
|
|
30
|
+
params: { limit?: number; offset?: number; search?: string } = {}
|
|
31
|
+
): Promise<TableData> {
|
|
32
|
+
const searchParams = new URLSearchParams()
|
|
33
|
+
if (params.limit) searchParams.set('limit', params.limit.toString())
|
|
34
|
+
if (params.offset) searchParams.set('offset', params.offset.toString())
|
|
35
|
+
if (params.search) searchParams.set('search', params.search)
|
|
36
|
+
|
|
37
|
+
const response = await fetch(`${API_BASE}/table/${tableName}?${searchParams}`)
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error('Failed to fetch table data')
|
|
40
|
+
}
|
|
41
|
+
const result = await response.json()
|
|
42
|
+
return {
|
|
43
|
+
data: result.rows || [],
|
|
44
|
+
hasMore: result.rows && result.rows.length === (params.limit || 50),
|
|
45
|
+
total: result.total,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function fetchArcheTypeData(
|
|
50
|
+
archeTypeName: string,
|
|
51
|
+
params: { limit?: number; offset?: number; search?: string; include_deleted?: boolean } = {}
|
|
52
|
+
): Promise<ArcheTypeData> {
|
|
53
|
+
const searchParams = new URLSearchParams()
|
|
54
|
+
if (params.limit) searchParams.set('limit', params.limit.toString())
|
|
55
|
+
if (params.offset) searchParams.set('offset', params.offset.toString())
|
|
56
|
+
if (params.search) searchParams.set('search', params.search)
|
|
57
|
+
if (params.include_deleted) searchParams.set('include_deleted', 'true')
|
|
58
|
+
|
|
59
|
+
const response = await fetch(`${API_BASE}/arche-type/${archeTypeName}?${searchParams}`)
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error('Failed to fetch archetype data')
|
|
62
|
+
}
|
|
63
|
+
const result = await response.json()
|
|
64
|
+
// Transform entities to flat records with id and component fields
|
|
65
|
+
const data = result.entities?.map((entity: any) => ({
|
|
66
|
+
id: entity.entityId,
|
|
67
|
+
...entity.components,
|
|
68
|
+
...(entity.deleted_at !== undefined ? { _deleted_at: entity.deleted_at } : {}),
|
|
69
|
+
})) || []
|
|
70
|
+
return {
|
|
71
|
+
data,
|
|
72
|
+
hasMore: result.entities && result.entities.length === (params.limit || 50),
|
|
73
|
+
total: result.total,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface EntityComponent {
|
|
78
|
+
id: string
|
|
79
|
+
name: string
|
|
80
|
+
type_id: string
|
|
81
|
+
data: unknown
|
|
82
|
+
created_at: string
|
|
83
|
+
updated_at: string
|
|
84
|
+
deleted_at: string | null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface EntityInspectorData {
|
|
88
|
+
entity: {
|
|
89
|
+
id: string
|
|
90
|
+
created_at: string
|
|
91
|
+
updated_at: string
|
|
92
|
+
deleted_at: string | null
|
|
93
|
+
}
|
|
94
|
+
components: EntityComponent[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface StudioStats {
|
|
98
|
+
entities: {
|
|
99
|
+
active: number
|
|
100
|
+
deleted: number
|
|
101
|
+
total: number
|
|
102
|
+
}
|
|
103
|
+
componentTypes: { name: string; count: number }[]
|
|
104
|
+
archetypes: { name: string; entityCount: number; componentCount: number }[]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function fetchStats(): Promise<StudioStats> {
|
|
108
|
+
const response = await fetch(`${API_BASE}/stats`)
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error('Failed to fetch stats')
|
|
111
|
+
}
|
|
112
|
+
return response.json()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ComponentTypeInfo {
|
|
116
|
+
name: string
|
|
117
|
+
entityCount: number
|
|
118
|
+
partitionTable: string
|
|
119
|
+
fields: string[]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function fetchComponents(): Promise<ComponentTypeInfo[]> {
|
|
123
|
+
const response = await fetch(`${API_BASE}/components`)
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error('Failed to fetch components')
|
|
126
|
+
}
|
|
127
|
+
const data = await response.json()
|
|
128
|
+
return data.components
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function fetchEntity(entityId: string): Promise<EntityInspectorData> {
|
|
132
|
+
const response = await fetch(`${API_BASE}/entity/${entityId}`)
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const error = await response.json().catch(() => ({ error: 'Failed to fetch entity' }))
|
|
135
|
+
throw new Error(error.error || 'Failed to fetch entity')
|
|
136
|
+
}
|
|
137
|
+
return response.json()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface QueryResult {
|
|
141
|
+
columns: string[]
|
|
142
|
+
rows: Record<string, unknown>[]
|
|
143
|
+
rowCount: number
|
|
144
|
+
duration: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function executeQuery(sql: string): Promise<QueryResult> {
|
|
148
|
+
const response = await fetch(`${API_BASE}/query`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({ sql }),
|
|
152
|
+
})
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const error = await response.json().catch(() => ({ error: 'Query failed' }))
|
|
155
|
+
throw new Error(error.error || 'Query failed')
|
|
156
|
+
}
|
|
157
|
+
return response.json()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function deleteTableRecords(tableName: string, ids: string[]): Promise<void> {
|
|
161
|
+
const response = await fetch(`${API_BASE}/table/${tableName}`, {
|
|
162
|
+
method: 'DELETE',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({ ids }),
|
|
167
|
+
})
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const error = await response.json().catch(() => ({ error: 'Failed to delete table records' }))
|
|
170
|
+
throw new Error(error.error || 'Failed to delete table records')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function deleteArcheTypeRecords(archeTypeName: string, entityIds: string[]): Promise<void> {
|
|
175
|
+
const response = await fetch(`${API_BASE}/arche-type/${archeTypeName}`, {
|
|
176
|
+
method: 'DELETE',
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({ entityIds }),
|
|
181
|
+
})
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
const error = await response.json().catch(() => ({ error: 'Failed to delete archetype records' }))
|
|
184
|
+
throw new Error(error.error || 'Failed to delete archetype records')
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx"
|
|
2
|
+
import { twMerge } from "tailwind-merge"
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function pluralize(count: number, singular: string, plural?: string): string {
|
|
9
|
+
if (count === 1) {
|
|
10
|
+
return singular
|
|
11
|
+
}
|
|
12
|
+
return plural || `${singular}s`
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
4
|
+
import { routes } from './routes.tsx'
|
|
5
|
+
import './index.css'
|
|
6
|
+
import { Toaster } from 'sonner'
|
|
7
|
+
|
|
8
|
+
const router = createBrowserRouter(routes, {
|
|
9
|
+
basename: '/studio',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
13
|
+
<React.StrictMode>
|
|
14
|
+
<RouterProvider router={router} />
|
|
15
|
+
<Toaster position="top-right" />
|
|
16
|
+
</React.StrictMode>,
|
|
17
|
+
)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useParams } from "react-router-dom";
|
|
2
|
+
import { type ColumnDef } from "@tanstack/react-table";
|
|
3
|
+
import { useStudioStore } from "../store/studio";
|
|
4
|
+
import { useArcheTypeSettings } from "../store/archeTypeSettings";
|
|
5
|
+
import { fetchArcheTypeData, deleteArcheTypeRecords } from "../lib/api";
|
|
6
|
+
import { PageContainer } from "../components/PageContainer";
|
|
7
|
+
import { SearchBar } from "../components/SearchBar";
|
|
8
|
+
import { DataTable } from "../components/DataTable";
|
|
9
|
+
import { Checkbox } from "../components/ui/checkbox";
|
|
10
|
+
import { useDataTable } from "../hooks/useDataTable";
|
|
11
|
+
import {
|
|
12
|
+
createSelectColumn,
|
|
13
|
+
createIdColumn,
|
|
14
|
+
createTextColumn,
|
|
15
|
+
} from "../utils/columnHelpers";
|
|
16
|
+
import { useMemo } from "react";
|
|
17
|
+
import { cn } from "../lib/utils";
|
|
18
|
+
|
|
19
|
+
interface ArcheTypeRecord {
|
|
20
|
+
id: string;
|
|
21
|
+
_deleted_at?: string | null;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findIndicatorName(
|
|
26
|
+
archeTypeName: string,
|
|
27
|
+
fields: { componentName: string }[]
|
|
28
|
+
): string | null {
|
|
29
|
+
if (fields.length === 0) return null;
|
|
30
|
+
const names = fields.map((f) => f.componentName);
|
|
31
|
+
const lower = archeTypeName.toLowerCase();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
names.find((n) => n.toLowerCase() === `${lower}tag`) ??
|
|
35
|
+
names.find((n) => n.toLowerCase() === `${lower}id`) ??
|
|
36
|
+
names.find((n) => n.toLowerCase().startsWith(lower)) ??
|
|
37
|
+
names.find((n) => n.toLowerCase().includes(lower)) ??
|
|
38
|
+
names[0] ??
|
|
39
|
+
null
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ArcheType() {
|
|
44
|
+
const { name } = useParams<{ name: string }>();
|
|
45
|
+
const { metadata } = useStudioStore();
|
|
46
|
+
const {
|
|
47
|
+
useRealDbFieldName,
|
|
48
|
+
autoExpandRow,
|
|
49
|
+
showDeleted,
|
|
50
|
+
setUseRealDbFieldName,
|
|
51
|
+
setAutoExpandRow,
|
|
52
|
+
setShowDeleted,
|
|
53
|
+
} = useArcheTypeSettings();
|
|
54
|
+
|
|
55
|
+
const fetchKey = `${name || ""}:${showDeleted}`;
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
data,
|
|
59
|
+
loading,
|
|
60
|
+
hasMore,
|
|
61
|
+
total,
|
|
62
|
+
search,
|
|
63
|
+
sorting,
|
|
64
|
+
selectedRecords,
|
|
65
|
+
setSearch,
|
|
66
|
+
setSorting,
|
|
67
|
+
setSelectedRecords,
|
|
68
|
+
handleDelete,
|
|
69
|
+
loadMoreRef,
|
|
70
|
+
} = useDataTable<ArcheTypeRecord>({
|
|
71
|
+
key: fetchKey,
|
|
72
|
+
fetchData: (params) =>
|
|
73
|
+
fetchArcheTypeData(name!, {
|
|
74
|
+
...params,
|
|
75
|
+
include_deleted: showDeleted,
|
|
76
|
+
}) as Promise<{
|
|
77
|
+
data: ArcheTypeRecord[];
|
|
78
|
+
hasMore: boolean;
|
|
79
|
+
total?: number;
|
|
80
|
+
}>,
|
|
81
|
+
deleteRecords: (ids) => deleteArcheTypeRecords(name!, ids),
|
|
82
|
+
fetchErrorMessage: "Failed to load archetype entities",
|
|
83
|
+
deleteErrorMessage: "Failed to delete archetype entities",
|
|
84
|
+
deleteSuccessMessage: "Deleted {count} {item}",
|
|
85
|
+
itemSingular: "entity",
|
|
86
|
+
itemPlural: "entities",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const archeTypeFields = metadata?.archeTypes[name || ""] || [];
|
|
90
|
+
const indicatorName = useMemo(
|
|
91
|
+
() => (name ? findIndicatorName(name, archeTypeFields) : null),
|
|
92
|
+
[name, archeTypeFields]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Preprocess data: transform Tag component values to "true" when useRealDbFieldName is enabled
|
|
96
|
+
const preprocessedData = useMemo(() => {
|
|
97
|
+
if (useRealDbFieldName) return data;
|
|
98
|
+
|
|
99
|
+
return data.map((record) => {
|
|
100
|
+
const newRecord = { ...record };
|
|
101
|
+
archeTypeFields.forEach((field) => {
|
|
102
|
+
if (field.componentName.endsWith("Tag")) {
|
|
103
|
+
newRecord[field.componentName] = "true";
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return newRecord;
|
|
107
|
+
});
|
|
108
|
+
}, [data, useRealDbFieldName, archeTypeFields]);
|
|
109
|
+
|
|
110
|
+
const columns: ColumnDef<ArcheTypeRecord>[] = useMemo(
|
|
111
|
+
() => [
|
|
112
|
+
createSelectColumn<ArcheTypeRecord>(),
|
|
113
|
+
createIdColumn<ArcheTypeRecord>({ linkToEntity: true }),
|
|
114
|
+
...archeTypeFields.map((field) => {
|
|
115
|
+
const isTagComponent = field.componentName.endsWith("Tag");
|
|
116
|
+
const shouldExtractValue = !(
|
|
117
|
+
useRealDbFieldName && isTagComponent
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return createTextColumn<ArcheTypeRecord>(
|
|
121
|
+
field.componentName,
|
|
122
|
+
useRealDbFieldName
|
|
123
|
+
? field.componentName
|
|
124
|
+
: field.fieldLabel || field.fieldName,
|
|
125
|
+
{ extractValue: shouldExtractValue, autoExpandRow }
|
|
126
|
+
);
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
[archeTypeFields, useRealDbFieldName, autoExpandRow]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const getRowClassName = (record: ArcheTypeRecord) =>
|
|
133
|
+
record._deleted_at ? "opacity-50 bg-destructive/5" : "";
|
|
134
|
+
|
|
135
|
+
if (!name) {
|
|
136
|
+
return <div className="p-8">Archetype name not found</div>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<PageContainer>
|
|
141
|
+
{/* Header with total count */}
|
|
142
|
+
<div className="mb-6">
|
|
143
|
+
<div className="flex items-baseline gap-3 mb-1">
|
|
144
|
+
<h1 className="text-3xl font-bold text-primary">{name}</h1>
|
|
145
|
+
{total !== null && (
|
|
146
|
+
<span className="text-lg text-muted-foreground font-mono tabular-nums">
|
|
147
|
+
{total.toLocaleString()} {total === 1 ? "entity" : "entities"}
|
|
148
|
+
</span>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
<p className="text-muted-foreground mb-4">
|
|
152
|
+
Browse and manage entities for the {name} archetype
|
|
153
|
+
</p>
|
|
154
|
+
|
|
155
|
+
{/* Component composition */}
|
|
156
|
+
{archeTypeFields.length > 0 && (
|
|
157
|
+
<div className="flex flex-wrap gap-1.5">
|
|
158
|
+
{archeTypeFields.map((field) => {
|
|
159
|
+
const isIndicator =
|
|
160
|
+
field.componentName === indicatorName;
|
|
161
|
+
const isOptional = !!field.nullable;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<span
|
|
165
|
+
key={field.componentName}
|
|
166
|
+
className={cn(
|
|
167
|
+
"inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium border",
|
|
168
|
+
isIndicator
|
|
169
|
+
? "bg-primary/15 text-primary border-primary/30"
|
|
170
|
+
: isOptional
|
|
171
|
+
? "bg-muted/50 text-muted-foreground border-border border-dashed"
|
|
172
|
+
: "bg-muted text-foreground border-border"
|
|
173
|
+
)}
|
|
174
|
+
title={
|
|
175
|
+
isIndicator
|
|
176
|
+
? "Indicator component"
|
|
177
|
+
: isOptional
|
|
178
|
+
? "Optional component"
|
|
179
|
+
: "Required component"
|
|
180
|
+
}
|
|
181
|
+
>
|
|
182
|
+
{field.componentName}
|
|
183
|
+
{isOptional && (
|
|
184
|
+
<span className="text-muted-foreground">?</span>
|
|
185
|
+
)}
|
|
186
|
+
</span>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<SearchBar
|
|
194
|
+
search={search}
|
|
195
|
+
onSearchChange={setSearch}
|
|
196
|
+
placeholder="Search entities..."
|
|
197
|
+
selectedCount={selectedRecords.size}
|
|
198
|
+
onDelete={handleDelete}
|
|
199
|
+
itemSingular="entity"
|
|
200
|
+
itemPlural="entities"
|
|
201
|
+
/>
|
|
202
|
+
<div className="flex items-center gap-6 mb-4">
|
|
203
|
+
<Checkbox
|
|
204
|
+
id="show-db-field-name"
|
|
205
|
+
label="Show real DB field name"
|
|
206
|
+
checked={useRealDbFieldName}
|
|
207
|
+
onChange={(e) => setUseRealDbFieldName(e.target.checked)}
|
|
208
|
+
/>
|
|
209
|
+
<Checkbox
|
|
210
|
+
id="auto-expand-row"
|
|
211
|
+
label="Auto expand row"
|
|
212
|
+
checked={autoExpandRow}
|
|
213
|
+
onChange={(e) => setAutoExpandRow(e.target.checked)}
|
|
214
|
+
/>
|
|
215
|
+
<Checkbox
|
|
216
|
+
id="show-deleted"
|
|
217
|
+
label="Show deleted"
|
|
218
|
+
checked={showDeleted}
|
|
219
|
+
onChange={(e) => setShowDeleted(e.target.checked)}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
<DataTable
|
|
223
|
+
data={preprocessedData}
|
|
224
|
+
columns={columns}
|
|
225
|
+
loading={loading}
|
|
226
|
+
hasMore={hasMore}
|
|
227
|
+
sorting={sorting}
|
|
228
|
+
onSortingChange={setSorting}
|
|
229
|
+
selectedRecords={selectedRecords}
|
|
230
|
+
onSelectionChange={setSelectedRecords}
|
|
231
|
+
getRecordId={(record) => record.id}
|
|
232
|
+
loadMoreRef={loadMoreRef}
|
|
233
|
+
emptyMessage="No entities found"
|
|
234
|
+
loadingMessage="Loading more entities..."
|
|
235
|
+
getRowClassName={getRowClassName}
|
|
236
|
+
/>
|
|
237
|
+
</PageContainer>
|
|
238
|
+
);
|
|
239
|
+
}
|