mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge branch 'main' into codex/refactor-generateflaskblueprint-sanitizer
This commit is contained in:
@@ -717,6 +717,348 @@
|
||||
"status": "supported",
|
||||
"source": "atoms"
|
||||
},
|
||||
{
|
||||
"type": "ArrowLeft",
|
||||
"name": "ArrowLeft",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ArrowLeft icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "ArrowRight",
|
||||
"name": "ArrowRight",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ArrowRight icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Check",
|
||||
"name": "Check",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Check icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "X",
|
||||
"name": "X",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "X icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Plus",
|
||||
"name": "Plus",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Plus icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Minus",
|
||||
"name": "Minus",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Minus icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Search",
|
||||
"name": "Search",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Search icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Filter",
|
||||
"name": "Filter",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Filter icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Download",
|
||||
"name": "Download",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Download icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Upload",
|
||||
"name": "Upload",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Upload icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Edit",
|
||||
"name": "Edit",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Edit icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Trash",
|
||||
"name": "Trash",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Trash icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Eye",
|
||||
"name": "Eye",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Eye icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "EyeOff",
|
||||
"name": "EyeOff",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "EyeOff icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "ChevronUp",
|
||||
"name": "ChevronUp",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronUp icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "ChevronDown",
|
||||
"name": "ChevronDown",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronDown icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "ChevronLeft",
|
||||
"name": "ChevronLeft",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronLeft icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "ChevronRight",
|
||||
"name": "ChevronRight",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "ChevronRight icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Settings",
|
||||
"name": "Settings",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Settings icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "User",
|
||||
"name": "User",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "User icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Bell",
|
||||
"name": "Bell",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Bell icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Mail",
|
||||
"name": "Mail",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Mail icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Calendar",
|
||||
"name": "Calendar",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Calendar icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Clock",
|
||||
"name": "Clock",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Clock icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Star",
|
||||
"name": "Star",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Star icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Heart",
|
||||
"name": "Heart",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Heart icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Share",
|
||||
"name": "Share",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Share icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Link",
|
||||
"name": "Link",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Link icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Copy",
|
||||
"name": "Copy",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Copy icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Save",
|
||||
"name": "Save",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Save icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "RefreshCw",
|
||||
"name": "RefreshCw",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "RefreshCw icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "AlertCircle",
|
||||
"name": "AlertCircle",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "AlertCircle icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Info",
|
||||
"name": "Info",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Info icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "HelpCircle",
|
||||
"name": "HelpCircle",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "HelpCircle icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Home",
|
||||
"name": "Home",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Home icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Menu",
|
||||
"name": "Menu",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "Menu icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "MoreVertical",
|
||||
"name": "MoreVertical",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "MoreVertical icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "MoreHorizontal",
|
||||
"name": "MoreHorizontal",
|
||||
"category": "display",
|
||||
"canHaveChildren": false,
|
||||
"description": "MoreHorizontal icon",
|
||||
"status": "supported",
|
||||
"source": "icons"
|
||||
},
|
||||
{
|
||||
"type": "Breadcrumb",
|
||||
"name": "Breadcrumb",
|
||||
@@ -1926,25 +2268,27 @@
|
||||
}
|
||||
],
|
||||
"statistics": {
|
||||
"total": 222,
|
||||
"supported": 209,
|
||||
"total": 239,
|
||||
"supported": 226,
|
||||
"planned": 0,
|
||||
"jsonCompatible": 13,
|
||||
"jsonCompatible": 50,
|
||||
"maybeJsonCompatible": 0,
|
||||
"byCategory": {
|
||||
"layout": 25,
|
||||
"input": 34,
|
||||
"display": 31,
|
||||
"navigation": 15,
|
||||
"feedback": 23,
|
||||
"data": 25,
|
||||
"custom": 69
|
||||
"layout": 24,
|
||||
"input": 26,
|
||||
"display": 64,
|
||||
"navigation": 12,
|
||||
"feedback": 21,
|
||||
"data": 27,
|
||||
"custom": 65
|
||||
},
|
||||
"bySource": {
|
||||
"atoms": 117,
|
||||
"molecules": 40,
|
||||
"organisms": 15,
|
||||
"ui": 50
|
||||
"molecules": 36,
|
||||
"organisms": 13,
|
||||
"ui": 25,
|
||||
"wrappers": 10,
|
||||
"icons": 38
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
schemas/json-components-registry-schema.json
Normal file
102
schemas/json-components-registry-schema.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "JSON Components Registry",
|
||||
"type": "object",
|
||||
"required": ["version", "description", "components"],
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"categories": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"category",
|
||||
"canHaveChildren",
|
||||
"description",
|
||||
"status",
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"export": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"canHaveChildren": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["atoms", "molecules", "organisms", "ui", "wrappers", "icons"]
|
||||
},
|
||||
"jsonCompatible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wrapperRequired": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wrapperComponent": {
|
||||
"type": "string"
|
||||
},
|
||||
"wrapperFor": {
|
||||
"type": "string"
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"replacedBy": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -69,6 +69,12 @@ export function usePWA() {
|
||||
setState(prev => ({ ...prev, isOnline: false }))
|
||||
}
|
||||
|
||||
const handleServiceWorkerMessage = (event: MessageEvent) => {
|
||||
if (event.data && event.data.type === 'CACHE_CLEARED') {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||||
window.addEventListener('appinstalled', handleAppInstalled)
|
||||
window.addEventListener('online', handleOnline)
|
||||
@@ -96,11 +102,7 @@ export function usePWA() {
|
||||
console.error('[PWA] Service Worker registration failed:', error)
|
||||
})
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'CACHE_CLEARED') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage)
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -108,6 +110,9 @@ export function usePWA() {
|
||||
window.removeEventListener('appinstalled', handleAppInstalled)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -87,7 +87,12 @@ export function analyzePerformance() {
|
||||
return null
|
||||
}
|
||||
|
||||
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
|
||||
const navigation = performance.getEntriesByType('navigation')[0] as
|
||||
| PerformanceNavigationTiming
|
||||
| undefined
|
||||
if (!navigation) {
|
||||
console.warn('[BUNDLE] ⚠️ Navigation performance entry not available')
|
||||
}
|
||||
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]
|
||||
|
||||
const jsResources = resources.filter(r => r.name.endsWith('.js'))
|
||||
@@ -97,9 +102,11 @@ export function analyzePerformance() {
|
||||
const totalCssSize = cssResources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
|
||||
|
||||
const analysis = {
|
||||
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
|
||||
loadComplete: navigation.loadEventEnd - navigation.fetchStart,
|
||||
ttfb: navigation.responseStart - navigation.fetchStart,
|
||||
domContentLoaded: navigation
|
||||
? navigation.domContentLoadedEventEnd - navigation.fetchStart
|
||||
: NaN,
|
||||
loadComplete: navigation ? navigation.loadEventEnd - navigation.fetchStart : NaN,
|
||||
ttfb: navigation ? navigation.responseStart - navigation.fetchStart : NaN,
|
||||
resources: {
|
||||
js: {
|
||||
count: jsResources.length,
|
||||
|
||||
73
src/lib/generators/__tests__/generateFlaskBlueprint.test.ts
Normal file
73
src/lib/generators/__tests__/generateFlaskBlueprint.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { FlaskBlueprint } from '@/types/project'
|
||||
|
||||
import { generateFlaskBlueprint } from '../generateFlaskBlueprint'
|
||||
|
||||
const isValidIdentifier = (name: string): boolean => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
|
||||
|
||||
const extractBlueprintVariable = (code: string): { variable: string; name: string } => {
|
||||
const match = code.match(/^([A-Za-z_][A-Za-z0-9_]*)_bp = Blueprint\('([^']+)'/m)
|
||||
if (!match) {
|
||||
throw new Error('Blueprint definition not found.')
|
||||
}
|
||||
return { variable: `${match[1]}_bp`, name: match[2] }
|
||||
}
|
||||
|
||||
const extractFunctionNames = (code: string): string[] => {
|
||||
return Array.from(code.matchAll(/^def ([A-Za-z_][A-Za-z0-9_]*)\(\):/gm)).map(match => match[1])
|
||||
}
|
||||
|
||||
const extractDecoratorBlueprints = (code: string): string[] => {
|
||||
return Array.from(code.matchAll(/^@([A-Za-z_][A-Za-z0-9_]*)\.route/gm)).map(match => match[1])
|
||||
}
|
||||
|
||||
describe('generateFlaskBlueprint identifier sanitization', () => {
|
||||
it('creates valid, consistent identifiers for tricky endpoint names', () => {
|
||||
const blueprint: FlaskBlueprint = {
|
||||
id: 'bp-1',
|
||||
name: 'User Auth',
|
||||
urlPrefix: '/auth',
|
||||
description: 'Auth endpoints',
|
||||
endpoints: [
|
||||
{
|
||||
id: 'ep-1',
|
||||
name: 'get-user',
|
||||
description: 'Fetch a user',
|
||||
method: 'GET',
|
||||
path: '/user'
|
||||
},
|
||||
{
|
||||
id: 'ep-2',
|
||||
name: '2fa',
|
||||
description: 'Two factor auth',
|
||||
method: 'POST',
|
||||
path: '/2fa'
|
||||
},
|
||||
{
|
||||
id: 'ep-3',
|
||||
name: 'user.v1',
|
||||
description: 'User v1 endpoint',
|
||||
method: 'GET',
|
||||
path: '/user/v1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const code = generateFlaskBlueprint(blueprint)
|
||||
const blueprintDefinition = extractBlueprintVariable(code)
|
||||
const functionNames = extractFunctionNames(code)
|
||||
const decoratorBlueprints = extractDecoratorBlueprints(code)
|
||||
|
||||
expect(isValidIdentifier(blueprintDefinition.name)).toBe(true)
|
||||
expect(isValidIdentifier(blueprintDefinition.variable)).toBe(true)
|
||||
expect(blueprintDefinition.variable).toBe('user_auth_bp')
|
||||
expect(blueprintDefinition.name).toBe('user_auth')
|
||||
expect(new Set(decoratorBlueprints)).toEqual(new Set([blueprintDefinition.variable]))
|
||||
|
||||
expect(functionNames).toEqual(['get_user', '_2fa', 'user_v1'])
|
||||
functionNames.forEach(name => {
|
||||
expect(isValidIdentifier(name)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FlaskConfig } from '@/types/project'
|
||||
import { generateFlaskBlueprint } from './generateFlaskBlueprint'
|
||||
import { sanitizeIdentifier } from './sanitizeIdentifier'
|
||||
|
||||
export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
const files: Record<string, string> = {}
|
||||
@@ -11,7 +12,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
appCode += `\n`
|
||||
|
||||
config.blueprints.forEach(blueprint => {
|
||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
appCode += `from blueprints.${blueprintVarName} import ${blueprintVarName}_bp\n`
|
||||
})
|
||||
|
||||
@@ -34,7 +35,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
}
|
||||
|
||||
config.blueprints.forEach(blueprint => {
|
||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
appCode += ` app.register_blueprint(${blueprintVarName}_bp)\n`
|
||||
})
|
||||
|
||||
@@ -50,7 +51,7 @@ export function generateFlaskApp(config: FlaskConfig): Record<string, string> {
|
||||
files['app.py'] = appCode
|
||||
|
||||
config.blueprints.forEach(blueprint => {
|
||||
const blueprintVarName = blueprint.name.toLowerCase().replace(/\s+/g, '_')
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
files[`blueprints/${blueprintVarName}.py`] = generateFlaskBlueprint(blueprint)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { FlaskBlueprint } from '@/types/project'
|
||||
import { sanitizeIdentifier } from './sanitizeIdentifier'
|
||||
|
||||
function toPythonIdentifier(value: string, fallback: string): string {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
let safe = normalized || fallback
|
||||
if (/^[0-9]/.test(safe)) {
|
||||
safe = `_${safe}`
|
||||
}
|
||||
return safe
|
||||
}
|
||||
|
||||
export function generateFlaskBlueprint(blueprint: FlaskBlueprint): string {
|
||||
let code = `from flask import Blueprint, request, jsonify\n`
|
||||
code += `from typing import Dict, Any\n\n`
|
||||
|
||||
const sanitizeName = (value: string): string => {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]+/gi, '_')
|
||||
.replace(/_+/g, '_')
|
||||
const safe = normalized.length > 0 ? normalized : '_'
|
||||
return /^[a-z_]/i.test(safe) ? safe : `_${safe}`
|
||||
}
|
||||
|
||||
const blueprintVarName = sanitizeName(blueprint.name)
|
||||
const blueprintVarName = sanitizeIdentifier(blueprint.name, { fallback: 'blueprint' })
|
||||
code += `${blueprintVarName}_bp = Blueprint('${blueprintVarName}', __name__, url_prefix='${blueprint.urlPrefix}')\n\n`
|
||||
|
||||
blueprint.endpoints.forEach(endpoint => {
|
||||
const functionName = sanitizeName(endpoint.name)
|
||||
const functionName = sanitizeIdentifier(endpoint.name, { fallback: 'endpoint' })
|
||||
code += `@${blueprintVarName}_bp.route('${endpoint.path}', methods=['${endpoint.method}'])\n`
|
||||
code += `def ${functionName}():\n`
|
||||
code += ` """\n`
|
||||
@@ -40,13 +45,14 @@ export function generateFlaskBlueprint(blueprint: FlaskBlueprint): string {
|
||||
|
||||
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
||||
endpoint.queryParams.forEach(param => {
|
||||
const paramVarName = sanitizeIdentifier(param.name, { fallback: 'param' })
|
||||
if (param.required) {
|
||||
code += ` ${param.name} = request.args.get('${param.name}')\n`
|
||||
code += ` if ${param.name} is None:\n`
|
||||
code += ` ${paramVarName} = request.args.get('${param.name}')\n`
|
||||
code += ` if ${paramVarName} is None:\n`
|
||||
code += ` return jsonify({'error': '${param.name} is required'}), 400\n\n`
|
||||
} else {
|
||||
const defaultVal = param.defaultValue || (param.type === 'string' ? "''" : param.type === 'number' ? '0' : 'None')
|
||||
code += ` ${param.name} = request.args.get('${param.name}', ${defaultVal})\n`
|
||||
code += ` ${paramVarName} = request.args.get('${param.name}', ${defaultVal})\n`
|
||||
}
|
||||
})
|
||||
code += `\n`
|
||||
|
||||
23
src/lib/generators/sanitizeIdentifier.ts
Normal file
23
src/lib/generators/sanitizeIdentifier.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
type SanitizeIdentifierOptions = {
|
||||
fallback?: string
|
||||
}
|
||||
|
||||
export function sanitizeIdentifier(value: string, options: SanitizeIdentifierOptions = {}): string {
|
||||
const fallback = options.fallback ?? 'identifier'
|
||||
const trimmed = value.trim()
|
||||
const normalized = trimmed
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.replace(/_+/g, '_')
|
||||
|
||||
if (!normalized) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if (/^[0-9]/.test(normalized)) {
|
||||
return `_${normalized}`
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
@@ -1,27 +1,47 @@
|
||||
import { ComponentType } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { InputOtp } from '@/components/ui/input-otp'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Alert as ShadcnAlert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { AlertDialog } from '@/components/ui/alert-dialog'
|
||||
import { AspectRatio } from '@/components/ui/aspect-ratio'
|
||||
import { Carousel } from '@/components/ui/carousel'
|
||||
import { ChartContainer as Chart } from '@/components/ui/chart'
|
||||
import { Collapsible } from '@/components/ui/collapsible'
|
||||
import { Command } from '@/components/ui/command'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { DropdownMenu } from '@/components/ui/dropdown-menu'
|
||||
import { Menubar } from '@/components/ui/menubar'
|
||||
import { NavigationMenu } from '@/components/ui/navigation-menu'
|
||||
import { Table as ShadcnTable, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
import { ResizablePanelGroup as Resizable } from '@/components/ui/resizable'
|
||||
import { Sheet } from '@/components/ui/sheet'
|
||||
import { Sidebar } from '@/components/ui/sidebar'
|
||||
import { Toaster as Sonner } from '@/components/ui/sonner'
|
||||
import { ToggleGroup } from '@/components/ui/toggle-group'
|
||||
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { CircularProgress, Divider, ProgressBar } from '@/components/atoms'
|
||||
import * as AtomComponents from '@/components/atoms'
|
||||
import * as MoleculeComponents from '@/components/molecules'
|
||||
import * as OrganismComponents from '@/components/organisms'
|
||||
import {
|
||||
ComponentBindingDialogWrapper,
|
||||
ComponentTreeWrapper,
|
||||
DataSourceEditorDialogWrapper,
|
||||
GitHubBuildStatusWrapper,
|
||||
LazyBarChartWrapper,
|
||||
LazyD3BarChartWrapper,
|
||||
LazyLineChartWrapper,
|
||||
@@ -49,6 +69,9 @@ interface JsonRegistryEntry {
|
||||
export?: string
|
||||
source?: string
|
||||
status?: string
|
||||
wrapperRequired?: boolean
|
||||
wrapperComponent?: string
|
||||
wrapperFor?: string
|
||||
deprecated?: DeprecatedComponentInfo
|
||||
}
|
||||
|
||||
@@ -63,6 +86,9 @@ export interface DeprecatedComponentInfo {
|
||||
|
||||
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
|
||||
|
||||
const getRegistryEntryName = (entry: JsonRegistryEntry): string | undefined =>
|
||||
entry.export ?? entry.name ?? entry.type
|
||||
|
||||
const buildRegistryFromNames = (
|
||||
names: string[],
|
||||
components: Record<string, ComponentType<any>>
|
||||
@@ -77,10 +103,18 @@ const buildRegistryFromNames = (
|
||||
}
|
||||
|
||||
const jsonRegistryEntries = jsonRegistry.components ?? []
|
||||
const registryEntryByType = new Map(
|
||||
jsonRegistryEntries
|
||||
.map((entry) => {
|
||||
const entryName = getRegistryEntryName(entry)
|
||||
return entryName ? [entryName, entry] : null
|
||||
})
|
||||
.filter((entry): entry is [string, JsonRegistryEntry] => Boolean(entry))
|
||||
)
|
||||
const atomComponentMap = AtomComponents as Record<string, ComponentType<any>>
|
||||
const deprecatedComponentInfo = jsonRegistryEntries.reduce<Record<string, DeprecatedComponentInfo>>(
|
||||
(acc, entry) => {
|
||||
const entryName = entry.export ?? entry.name ?? entry.type
|
||||
const entryName = getRegistryEntryName(entry)
|
||||
if (!entryName) {
|
||||
return acc
|
||||
}
|
||||
@@ -93,15 +127,27 @@ const deprecatedComponentInfo = jsonRegistryEntries.reduce<Record<string, Deprec
|
||||
)
|
||||
const atomRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'atoms')
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const moleculeRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'molecules')
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const organismRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'organisms')
|
||||
.map((entry) => entry.export ?? entry.name ?? entry.type)
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const shadcnRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'ui')
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const wrapperRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'wrappers')
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
const iconRegistryNames = jsonRegistryEntries
|
||||
.filter((entry) => entry.source === 'icons')
|
||||
.map((entry) => getRegistryEntryName(entry))
|
||||
.filter((name): name is string => Boolean(name))
|
||||
|
||||
export const primitiveComponents: UIComponentRegistry = {
|
||||
@@ -123,9 +169,17 @@ export const primitiveComponents: UIComponentRegistry = {
|
||||
nav: 'nav' as any,
|
||||
}
|
||||
|
||||
export const shadcnComponents: UIComponentRegistry = {
|
||||
const shadcnComponentMap: Record<string, ComponentType<any>> = {
|
||||
AlertDialog,
|
||||
AspectRatio,
|
||||
Button,
|
||||
Carousel,
|
||||
Chart,
|
||||
Collapsible,
|
||||
Command,
|
||||
DropdownMenu,
|
||||
Input,
|
||||
InputOtp,
|
||||
Textarea,
|
||||
Label,
|
||||
Card,
|
||||
@@ -164,13 +218,26 @@ export const shadcnComponents: UIComponentRegistry = {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Menubar,
|
||||
NavigationMenu,
|
||||
Skeleton: ShadcnSkeleton,
|
||||
Pagination,
|
||||
Progress,
|
||||
Resizable,
|
||||
Sheet,
|
||||
Sidebar,
|
||||
Sonner,
|
||||
ToggleGroup,
|
||||
Avatar: ShadcnAvatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
}
|
||||
|
||||
export const shadcnComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
shadcnRegistryNames,
|
||||
shadcnComponentMap
|
||||
)
|
||||
|
||||
export const atomComponents: UIComponentRegistry = {
|
||||
...buildRegistryFromNames(
|
||||
atomRegistryNames,
|
||||
@@ -208,16 +275,25 @@ export const organismComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
OrganismComponents as Record<string, ComponentType<any>>
|
||||
)
|
||||
|
||||
export const jsonWrapperComponents: UIComponentRegistry = {
|
||||
SaveIndicator: SaveIndicatorWrapper,
|
||||
LazyBarChart: LazyBarChartWrapper,
|
||||
LazyLineChart: LazyLineChartWrapper,
|
||||
LazyD3BarChart: LazyD3BarChartWrapper,
|
||||
SeedDataManager: SeedDataManagerWrapper,
|
||||
StorageSettings: StorageSettingsWrapper,
|
||||
const wrapperComponentMap: Record<string, ComponentType<any>> = {
|
||||
ComponentBindingDialogWrapper,
|
||||
ComponentTreeWrapper,
|
||||
DataSourceEditorDialogWrapper,
|
||||
GitHubBuildStatusWrapper,
|
||||
SaveIndicatorWrapper,
|
||||
LazyBarChartWrapper,
|
||||
LazyLineChartWrapper,
|
||||
LazyD3BarChartWrapper,
|
||||
SeedDataManagerWrapper,
|
||||
StorageSettingsWrapper,
|
||||
}
|
||||
|
||||
export const iconComponents: UIComponentRegistry = {
|
||||
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
wrapperRegistryNames,
|
||||
wrapperComponentMap
|
||||
)
|
||||
|
||||
const iconComponentMap: Record<string, ComponentType<any>> = {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
@@ -258,6 +334,11 @@ export const iconComponents: UIComponentRegistry = {
|
||||
MoreHorizontal: DotsThree,
|
||||
}
|
||||
|
||||
export const iconComponents: UIComponentRegistry = buildRegistryFromNames(
|
||||
iconRegistryNames,
|
||||
iconComponentMap
|
||||
)
|
||||
|
||||
export const uiComponentRegistry: UIComponentRegistry = {
|
||||
...primitiveComponents,
|
||||
...shadcnComponents,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { FlaskBackendAdapter } from '../flask-backend-adapter'
|
||||
|
||||
type MockResponse = {
|
||||
ok: boolean
|
||||
status: number
|
||||
statusText: string
|
||||
text: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
const createMockResponse = (status: number, body: string): MockResponse => ({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: status === 204 ? 'No Content' : 'OK',
|
||||
text: vi.fn().mockResolvedValue(body),
|
||||
})
|
||||
|
||||
describe('FlaskBackendAdapter.request', () => {
|
||||
const baseUrl = 'http://example.test'
|
||||
let fetchMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('resolves delete/clear when response is 204 or empty body', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(createMockResponse(204, '') as unknown as Response)
|
||||
.mockResolvedValueOnce(createMockResponse(200, '') as unknown as Response)
|
||||
|
||||
const adapter = new FlaskBackendAdapter(baseUrl)
|
||||
|
||||
await expect(adapter.delete('example-key')).resolves.toBeUndefined()
|
||||
await expect(adapter.clear()).resolves.toBeUndefined()
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -25,11 +25,28 @@ export class FlaskBackendAdapter implements StorageAdapter {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||
throw new Error(error.error || `HTTP ${response.status}`)
|
||||
let errorMessage = response.statusText
|
||||
try {
|
||||
const errorText = await response.text()
|
||||
if (errorText) {
|
||||
try {
|
||||
const parsed = JSON.parse(errorText) as { error?: string }
|
||||
errorMessage = parsed.error || errorText
|
||||
} catch {
|
||||
errorMessage = errorText
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore error parsing failures
|
||||
}
|
||||
throw new Error(errorMessage || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
const responseText = await response.text()
|
||||
if (!responseText) {
|
||||
return undefined as T
|
||||
}
|
||||
return JSON.parse(responseText) as T
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeoutId)
|
||||
if (error.name === 'AbortError') {
|
||||
|
||||
145
src/lib/unified-storage.test.ts
Normal file
145
src/lib/unified-storage.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
callOrder,
|
||||
mockFlaskGet,
|
||||
mockIndexedGet,
|
||||
mockSQLiteGet,
|
||||
mockSparkGet,
|
||||
MockFlaskBackendAdapter,
|
||||
MockIndexedDBAdapter,
|
||||
MockSQLiteAdapter,
|
||||
MockSparkKVAdapter
|
||||
} = vi.hoisted(() => {
|
||||
const callOrder: string[] = []
|
||||
const mockFlaskGet = vi.fn<[], Promise<unknown>>()
|
||||
const mockIndexedGet = vi.fn<[], Promise<unknown>>()
|
||||
const mockSQLiteGet = vi.fn<[], Promise<unknown>>()
|
||||
const mockSparkGet = vi.fn<[], Promise<unknown>>()
|
||||
|
||||
class MockFlaskBackendAdapter {
|
||||
constructor() {
|
||||
callOrder.push('flask')
|
||||
}
|
||||
|
||||
get = mockFlaskGet
|
||||
}
|
||||
|
||||
class MockIndexedDBAdapter {
|
||||
constructor() {
|
||||
callOrder.push('indexeddb')
|
||||
}
|
||||
|
||||
get = mockIndexedGet
|
||||
}
|
||||
|
||||
class MockSQLiteAdapter {
|
||||
constructor() {
|
||||
callOrder.push('sqlite')
|
||||
}
|
||||
|
||||
get = mockSQLiteGet
|
||||
}
|
||||
|
||||
class MockSparkKVAdapter {
|
||||
constructor() {
|
||||
callOrder.push('sparkkv')
|
||||
}
|
||||
|
||||
get = mockSparkGet
|
||||
}
|
||||
|
||||
return {
|
||||
callOrder,
|
||||
mockFlaskGet,
|
||||
mockIndexedGet,
|
||||
mockSQLiteGet,
|
||||
mockSparkGet,
|
||||
MockFlaskBackendAdapter,
|
||||
MockIndexedDBAdapter,
|
||||
MockSQLiteAdapter,
|
||||
MockSparkKVAdapter
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./unified-storage-adapters', () => ({
|
||||
FlaskBackendAdapter: MockFlaskBackendAdapter,
|
||||
IndexedDBAdapter: MockIndexedDBAdapter,
|
||||
SQLiteAdapter: MockSQLiteAdapter,
|
||||
SparkKVAdapter: MockSparkKVAdapter
|
||||
}))
|
||||
|
||||
const createLocalStorageMock = () => {
|
||||
const store = new Map<string, string>()
|
||||
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store.set(key, value)
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
store.delete(key)
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
store.clear()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('UnifiedStorage.detectAndInitialize', () => {
|
||||
let localStorageMock: ReturnType<typeof createLocalStorageMock>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
callOrder.length = 0
|
||||
mockFlaskGet.mockReset()
|
||||
mockIndexedGet.mockReset()
|
||||
mockSQLiteGet.mockReset()
|
||||
mockSparkGet.mockReset()
|
||||
|
||||
localStorageMock = createLocalStorageMock()
|
||||
vi.stubGlobal('localStorage', localStorageMock)
|
||||
vi.stubGlobal('window', { spark: undefined })
|
||||
|
||||
if (!(import.meta as { env?: Record<string, string | undefined> }).env) {
|
||||
;(import.meta as { env?: Record<string, string | undefined> }).env = {}
|
||||
}
|
||||
})
|
||||
|
||||
it('tries Flask before IndexedDB when prefer-flask is set', async () => {
|
||||
localStorageMock.setItem('codeforge-prefer-flask', 'true')
|
||||
mockFlaskGet.mockRejectedValue(new Error('flask down'))
|
||||
mockIndexedGet.mockResolvedValue(undefined)
|
||||
vi.stubGlobal('indexedDB', {})
|
||||
|
||||
const { unifiedStorage } = await import('./unified-storage')
|
||||
await unifiedStorage.getBackend()
|
||||
|
||||
expect(callOrder[0]).toBe('flask')
|
||||
expect(callOrder).toContain('indexeddb')
|
||||
})
|
||||
|
||||
it('falls back to IndexedDB when Flask initialization fails', async () => {
|
||||
localStorageMock.setItem('codeforge-prefer-flask', 'true')
|
||||
mockFlaskGet.mockRejectedValue(new Error('flask down'))
|
||||
mockIndexedGet.mockResolvedValue(undefined)
|
||||
vi.stubGlobal('indexedDB', {})
|
||||
|
||||
const { unifiedStorage } = await import('./unified-storage')
|
||||
const backend = await unifiedStorage.getBackend()
|
||||
|
||||
expect(backend).toBe('indexeddb')
|
||||
})
|
||||
|
||||
it('honors prefer-sqlite when configured', async () => {
|
||||
localStorageMock.setItem('codeforge-prefer-sqlite', 'true')
|
||||
mockSQLiteGet.mockResolvedValue(undefined)
|
||||
delete (globalThis as { indexedDB?: unknown }).indexedDB
|
||||
|
||||
const { unifiedStorage } = await import('./unified-storage')
|
||||
const backend = await unifiedStorage.getBackend()
|
||||
|
||||
expect(backend).toBe('sqlite')
|
||||
expect(callOrder).toContain('sqlite')
|
||||
})
|
||||
})
|
||||
@@ -19,6 +19,23 @@ class UnifiedStorage {
|
||||
const flaskEnvUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const preferSQLite = localStorage.getItem('codeforge-prefer-sqlite') === 'true'
|
||||
|
||||
if (preferFlask || flaskEnvUrl) {
|
||||
try {
|
||||
console.log('[Storage] Flask backend explicitly configured, attempting to initialize...')
|
||||
const flaskAdapter = new FlaskBackendAdapter(flaskEnvUrl)
|
||||
await Promise.race([
|
||||
flaskAdapter.get('_health_check'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Flask connection timeout')), 2000))
|
||||
])
|
||||
this.adapter = flaskAdapter
|
||||
this.backend = 'flask'
|
||||
console.log('[Storage] ✓ Using Flask backend')
|
||||
return
|
||||
} catch (error) {
|
||||
console.warn('[Storage] Flask backend not available, falling back to IndexedDB:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof indexedDB !== 'undefined') {
|
||||
try {
|
||||
console.log('[Storage] Initializing default IndexedDB backend...')
|
||||
@@ -33,26 +50,6 @@ class UnifiedStorage {
|
||||
}
|
||||
}
|
||||
|
||||
if (preferFlask || flaskEnvUrl) {
|
||||
try {
|
||||
console.log('[Storage] Flask backend explicitly configured, attempting to initialize...')
|
||||
const flaskAdapter = new FlaskBackendAdapter(flaskEnvUrl)
|
||||
const testResponse = await Promise.race([
|
||||
flaskAdapter.get('_health_check'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Flask connection timeout')), 2000))
|
||||
])
|
||||
this.adapter = flaskAdapter
|
||||
this.backend = 'flask'
|
||||
console.log('[Storage] ✓ Using Flask backend')
|
||||
return
|
||||
} catch (error) {
|
||||
console.warn('[Storage] Flask backend not available, already using IndexedDB:', error)
|
||||
if (this.adapter && this.backend === 'indexeddb') {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preferSQLite) {
|
||||
try {
|
||||
console.log('[Storage] SQLite fallback, attempting to initialize...')
|
||||
|
||||
Reference in New Issue
Block a user