feat: Add hooks for blob storage and cached data management

- Implemented `useBlobStorage` hook for handling blob storage operations including upload, download, delete, list, and metadata retrieval.
- Created `useCachedData` hook for managing cached data with automatic serialization and error handling.
- Added `useDBAL` hook to ensure DBAL initialization before performing operations.
- Introduced `useKVStore` hook for key-value store operations with tenant and user context.
- Added tests for user retrieval with tenant filtering.
- Refactored package export functionality into modular files for better maintainability.
- Implemented utility functions for rendering components and executing Lua scripts.
- Established a declarative component renderer with support for component configuration and Lua script registration.
- Updated access rules to streamline credential access permissions.
This commit is contained in:
2025-12-25 18:40:14 +00:00
parent d6e9197ce7
commit 4c70eef94b
23 changed files with 759 additions and 255 deletions

View File

@@ -0,0 +1,102 @@
import { useCallback } from 'react'
import { dbal } from '@/lib/dbal-integration'
import { toast } from 'sonner'
import { useDBAL } from './use-dbal'
/**
* Hook for blob storage operations
*/
export function useBlobStorage() {
const { isReady } = useDBAL()
const upload = useCallback(
async (key: string, data: Buffer | Uint8Array, metadata?: Record<string, string>) => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
await dbal.blobUpload(key, data, metadata)
toast.success(`Uploaded: ${key}`)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`Upload Error: ${errorInfo.message}`)
throw err
}
},
[isReady]
)
const download = useCallback(
async (key: string): Promise<Buffer> => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
return await dbal.blobDownload(key)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`Download Error: ${errorInfo.message}`)
throw err
}
},
[isReady]
)
const del = useCallback(
async (key: string) => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
await dbal.blobDelete(key)
toast.success(`Deleted: ${key}`)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`Delete Error: ${errorInfo.message}`)
throw err
}
},
[isReady]
)
const list = useCallback(
async (prefix?: string): Promise<string[]> => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
return await dbal.blobList(prefix)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`List Error: ${errorInfo.message}`)
throw err
}
},
[isReady]
)
const getMetadata = useCallback(
async (key: string): Promise<Record<string, string>> => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
return await dbal.blobGetMetadata(key)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`Get Metadata Error: ${errorInfo.message}`)
throw err
}
},
[isReady]
)
return {
isReady,
upload,
download,
delete: del,
list,
getMetadata,
}
}

View File

@@ -0,0 +1,69 @@
import { useCallback, useEffect, useState } from 'react'
import { dbal } from '@/lib/dbal-integration'
import { useKVStore } from './use-kv-store'
/**
* Hook for storing and retrieving cached data with automatic serialization
*/
export function useCachedData<T>(key: string, tenantId?: string, userId?: string) {
const kv = useKVStore(tenantId, userId)
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const loadData = async () => {
if (!kv.isReady) return
try {
setLoading(true)
const cached = await kv.get<T>(key)
setData(cached)
setError(null)
} catch (err) {
const errorInfo = dbal.handleError(err)
setError(errorInfo.message)
} finally {
setLoading(false)
}
}
loadData()
}, [key, kv.isReady])
const save = useCallback(
async (newData: T, ttl?: number) => {
try {
await kv.set(key, newData, ttl)
setData(newData)
setError(null)
} catch (err) {
const errorInfo = dbal.handleError(err)
setError(errorInfo.message)
throw err
}
},
[key, kv]
)
const clear = useCallback(async () => {
try {
await kv.delete(key)
setData(null)
setError(null)
} catch (err) {
const errorInfo = dbal.handleError(err)
setError(errorInfo.message)
throw err
}
}, [key, kv])
return {
data,
loading,
error,
save,
clear,
isReady: kv.isReady,
}
}

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from 'react'
import { dbal } from '@/lib/dbal-integration'
/**
* Hook to ensure DBAL is initialized
*/
export function useDBAL() {
const [isReady, setIsReady] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const init = async () => {
try {
if (!dbal.isInitialized()) {
await dbal.initialize()
}
setIsReady(true)
} catch (err) {
const errorInfo = dbal.handleError(err)
setError(errorInfo.message)
console.error('DBAL initialization failed:', err)
}
}
init()
}, [])
return { isReady, error }
}

View File

@@ -0,0 +1,100 @@
import { useCallback } from 'react'
import { dbal } from '@/lib/dbal-integration'
import { toast } from 'sonner'
import { useDBAL } from './use-dbal'
/**
* Hook for KV store operations
*/
export function useKVStore(tenantId: string = 'default', userId: string = 'system') {
const { isReady } = useDBAL()
const set = useCallback(
async (key: string, value: any, ttl?: number) => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
await dbal.kvSet(key, value, ttl, tenantId, userId)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`KV Set Error: ${errorInfo.message}`)
throw err
}
},
[isReady, tenantId, userId]
)
const get = useCallback(
async <T = any>(key: string): Promise<T | null> => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
return await dbal.kvGet<T>(key, tenantId, userId)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`KV Get Error: ${errorInfo.message}`)
throw err
}
},
[isReady, tenantId, userId]
)
const del = useCallback(
async (key: string): Promise<boolean> => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
return await dbal.kvDelete(key, tenantId, userId)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`KV Delete Error: ${errorInfo.message}`)
throw err
}
},
[isReady, tenantId, userId]
)
const listAdd = useCallback(
async (key: string, items: any[]) => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
await dbal.kvListAdd(key, items, tenantId, userId)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`KV List Add Error: ${errorInfo.message}`)
throw err
}
},
[isReady, tenantId, userId]
)
const listGet = useCallback(
async (key: string, start?: number, end?: number): Promise<any[]> => {
if (!isReady) {
throw new Error('DBAL not ready')
}
try {
return await dbal.kvListGet(key, tenantId, userId, start, end)
} catch (err) {
const errorInfo = dbal.handleError(err)
toast.error(`KV List Get Error: ${errorInfo.message}`)
throw err
}
},
[isReady, tenantId, userId]
)
return {
isReady,
set,
get,
delete: del,
listAdd,
listGet,
}
}

View File

@@ -1,7 +1,8 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
const mockRead = vi.fn()
const mockAdapter = { read: mockRead }
const mockFindFirst = vi.fn()
const mockAdapter = { read: mockRead, findFirst: mockFindFirst }
vi.mock('../dbal-client', () => ({
getAdapter: () => mockAdapter,
@@ -12,6 +13,7 @@ import { getUserById } from './get-user-by-id'
describe('getUserById', () => {
beforeEach(() => {
mockRead.mockReset()
mockFindFirst.mockReset()
})
it.each([
@@ -79,6 +81,45 @@ describe('getUserById', () => {
const result = await getUserById(userId)
expect(mockRead).toHaveBeenCalledWith('User', userId)
expect(mockFindFirst).not.toHaveBeenCalled()
expect(result).toEqual(expected)
})
it.each([
{
name: 'use tenant filter when provided',
userId: 'user_3',
tenantId: 'tenant_2',
dbData: {
id: 'user_3',
username: 'tenant-user',
email: 'tenant@example.com',
role: 'admin',
profilePicture: null,
bio: null,
createdAt: BigInt(3000),
tenantId: 'tenant_2',
isInstanceOwner: false,
},
expected: {
id: 'user_3',
username: 'tenant-user',
email: 'tenant@example.com',
role: 'admin',
profilePicture: undefined,
bio: undefined,
createdAt: 3000,
tenantId: 'tenant_2',
isInstanceOwner: false,
},
},
])('should $name', async ({ userId, tenantId, dbData, expected }) => {
mockFindFirst.mockResolvedValue(dbData)
const result = await getUserById(userId, { tenantId })
expect(mockFindFirst).toHaveBeenCalledWith('User', { where: { id: userId, tenantId } })
expect(mockRead).not.toHaveBeenCalled()
expect(result).toEqual(expected)
})
})

View File

@@ -1,254 +1,6 @@
import JSZip from 'jszip'
import type { PackageManifest, PackageContent } from './package-types'
import type { DatabaseSchema } from './database'
export type { AssetFile, ExportPackageOptions } from './package-export/types'
export interface ExportPackageOptions {
includeAssets?: boolean
includeSchemas?: boolean
includePages?: boolean
includeWorkflows?: boolean
includeLuaScripts?: boolean
includeComponentHierarchy?: boolean
includeComponentConfigs?: boolean
includeCssClasses?: boolean
includeDropdownConfigs?: boolean
includeSeedData?: boolean
}
export interface AssetFile {
path: string
blob: Blob
type: 'image' | 'video' | 'audio' | 'document'
}
export async function exportPackageAsZip(
manifest: PackageManifest,
content: PackageContent,
assets: AssetFile[] = [],
options: ExportPackageOptions = {}
): Promise<Blob> {
const zip = new JSZip()
const opts = {
includeAssets: true,
includeSchemas: true,
includePages: true,
includeWorkflows: true,
includeLuaScripts: true,
includeComponentHierarchy: true,
includeComponentConfigs: true,
includeCssClasses: true,
includeDropdownConfigs: true,
includeSeedData: true,
...options,
}
zip.file('manifest.json', JSON.stringify(manifest, null, 2))
const packageContent: Partial<PackageContent> = {}
if (opts.includeSchemas && content.schemas) {
packageContent.schemas = content.schemas
}
if (opts.includePages && content.pages) {
packageContent.pages = content.pages
}
if (opts.includeWorkflows && content.workflows) {
packageContent.workflows = content.workflows
}
if (opts.includeLuaScripts && content.luaScripts) {
packageContent.luaScripts = content.luaScripts
}
if (opts.includeComponentHierarchy && content.componentHierarchy) {
packageContent.componentHierarchy = content.componentHierarchy
}
if (opts.includeComponentConfigs && content.componentConfigs) {
packageContent.componentConfigs = content.componentConfigs
}
if (opts.includeCssClasses && content.cssClasses) {
packageContent.cssClasses = content.cssClasses
}
if (opts.includeDropdownConfigs && content.dropdownConfigs) {
packageContent.dropdownConfigs = content.dropdownConfigs
}
if (opts.includeSeedData && content.seedData) {
packageContent.seedData = content.seedData
}
zip.file('content.json', JSON.stringify(packageContent, null, 2))
if (opts.includeAssets && assets.length > 0) {
const assetsFolder = zip.folder('assets')
if (assetsFolder) {
for (const asset of assets) {
const typeFolder = assetsFolder.folder(asset.type + 's')
if (typeFolder) {
const fileName = asset.path.split('/').pop() || 'unnamed'
typeFolder.file(fileName, asset.blob)
}
}
const assetManifest = assets.map(asset => ({
originalPath: asset.path,
type: asset.type,
fileName: asset.path.split('/').pop(),
}))
assetsFolder.file('asset-manifest.json', JSON.stringify(assetManifest, null, 2))
}
}
zip.file('README.md', generateReadme(manifest, content))
const blob = await zip.generateAsync({ type: 'blob' })
return blob
}
export async function importPackageFromZip(zipFile: File): Promise<{
manifest: PackageManifest
content: PackageContent
assets: Array<{ path: string; blob: Blob; type: 'image' | 'video' | 'audio' | 'document' }>
}> {
const zip = await JSZip.loadAsync(zipFile)
const manifestFile = zip.file('manifest.json')
if (!manifestFile) {
throw new Error('Invalid package: manifest.json not found')
}
const manifestText = await manifestFile.async('text')
const manifest: PackageManifest = JSON.parse(manifestText)
const contentFile = zip.file('content.json')
if (!contentFile) {
throw new Error('Invalid package: content.json not found')
}
const contentText = await contentFile.async('text')
const content: PackageContent = JSON.parse(contentText)
const assets: Array<{ path: string; blob: Blob; type: 'image' | 'video' | 'audio' | 'document' }> = []
const assetManifestFile = zip.file('assets/asset-manifest.json')
if (assetManifestFile) {
const assetManifestText = await assetManifestFile.async('text')
const assetManifest: Array<{ originalPath: string; type: string; fileName: string }> = JSON.parse(assetManifestText)
for (const assetInfo of assetManifest) {
const assetPath = `assets/${assetInfo.type}s/${assetInfo.fileName}`
const assetFile = zip.file(assetPath)
if (assetFile) {
const blob = await assetFile.async('blob')
assets.push({
path: assetInfo.originalPath,
blob,
type: assetInfo.type as 'image' | 'video' | 'audio' | 'document',
})
}
}
}
return { manifest, content, assets }
}
export function downloadZip(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
function generateReadme(manifest: PackageManifest, content: PackageContent): string {
return `# ${manifest.name}
Version: ${manifest.version}
Author: ${manifest.author}
Category: ${manifest.category}
## Description
${manifest.description}
## Contents
- **Schemas**: ${content.schemas?.length || 0} data models
- **Pages**: ${content.pages?.length || 0} page configurations
- **Workflows**: ${content.workflows?.length || 0} workflows
- **Lua Scripts**: ${content.luaScripts?.length || 0} scripts
- **Components**: ${Object.keys(content.componentHierarchy || {}).length} component hierarchies
- **CSS Classes**: ${content.cssClasses?.length || 0} CSS categories
- **Dropdown Configs**: ${content.dropdownConfigs?.length || 0} dropdown configurations
## Tags
${manifest.tags.join(', ')}
## Installation
Import this package through the MetaBuilder Package Manager.
## Dependencies
${manifest.dependencies.length > 0 ? manifest.dependencies.join(', ') : 'None'}
---
Generated by MetaBuilder Package Exporter
`
}
export async function exportDatabaseSnapshot(
schemas: any[],
pages: any[],
workflows: any[],
luaScripts: any[],
componentHierarchy: Record<string, any>,
componentConfigs: Record<string, any>,
cssClasses: any[],
dropdownConfigs: any[],
assets: AssetFile[] = []
): Promise<Blob> {
const manifest: PackageManifest = {
id: `snapshot_${Date.now()}`,
name: 'Database Snapshot',
version: '1.0.0',
description: 'Complete database snapshot export',
author: 'User Export',
category: 'other',
icon: '💾',
screenshots: [],
tags: ['snapshot', 'backup', 'export'],
dependencies: [],
createdAt: Date.now(),
updatedAt: Date.now(),
downloadCount: 0,
rating: 0,
installed: false,
}
const content: PackageContent = {
schemas,
pages,
workflows,
luaScripts,
componentHierarchy,
componentConfigs,
cssClasses,
dropdownConfigs,
}
return exportPackageAsZip(manifest, content, assets)
}
export { exportPackageAsZip } from './package-export/export-package-as-zip'
export { importPackageFromZip } from './package-export/import-package-from-zip'
export { downloadZip } from './package-export/download-zip'
export { exportDatabaseSnapshot } from './package-export/export-database-snapshot'

View File

@@ -0,0 +1,10 @@
export function downloadZip(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}

View File

@@ -0,0 +1,45 @@
import type { AssetFile, PackageContent, PackageManifest } from './types'
import { exportPackageAsZip } from './export-package-as-zip'
export async function exportDatabaseSnapshot(
schemas: any[],
pages: any[],
workflows: any[],
luaScripts: any[],
componentHierarchy: Record<string, any>,
componentConfigs: Record<string, any>,
cssClasses: any[],
dropdownConfigs: any[],
assets: AssetFile[] = []
): Promise<Blob> {
const manifest: PackageManifest = {
id: `snapshot_${Date.now()}`,
name: 'Database Snapshot',
version: '1.0.0',
description: 'Complete database snapshot export',
author: 'User Export',
category: 'other',
icon: '💾',
screenshots: [],
tags: ['snapshot', 'backup', 'export'],
dependencies: [],
createdAt: Date.now(),
updatedAt: Date.now(),
downloadCount: 0,
rating: 0,
installed: false,
}
const content: PackageContent = {
schemas,
pages,
workflows,
luaScripts,
componentHierarchy,
componentConfigs,
cssClasses,
dropdownConfigs,
}
return exportPackageAsZip(manifest, content, assets)
}

View File

@@ -0,0 +1,94 @@
import JSZip from 'jszip'
import type { AssetFile, ExportPackageOptions, PackageContent, PackageManifest } from './types'
import { generateReadme } from './generate-readme'
export async function exportPackageAsZip(
manifest: PackageManifest,
content: PackageContent,
assets: AssetFile[] = [],
options: ExportPackageOptions = {}
): Promise<Blob> {
const zip = new JSZip()
const opts = {
includeAssets: true,
includeSchemas: true,
includePages: true,
includeWorkflows: true,
includeLuaScripts: true,
includeComponentHierarchy: true,
includeComponentConfigs: true,
includeCssClasses: true,
includeDropdownConfigs: true,
includeSeedData: true,
...options,
}
zip.file('manifest.json', JSON.stringify(manifest, null, 2))
const packageContent: Partial<PackageContent> = {}
if (opts.includeSchemas && content.schemas) {
packageContent.schemas = content.schemas
}
if (opts.includePages && content.pages) {
packageContent.pages = content.pages
}
if (opts.includeWorkflows && content.workflows) {
packageContent.workflows = content.workflows
}
if (opts.includeLuaScripts && content.luaScripts) {
packageContent.luaScripts = content.luaScripts
}
if (opts.includeComponentHierarchy && content.componentHierarchy) {
packageContent.componentHierarchy = content.componentHierarchy
}
if (opts.includeComponentConfigs && content.componentConfigs) {
packageContent.componentConfigs = content.componentConfigs
}
if (opts.includeCssClasses && content.cssClasses) {
packageContent.cssClasses = content.cssClasses
}
if (opts.includeDropdownConfigs && content.dropdownConfigs) {
packageContent.dropdownConfigs = content.dropdownConfigs
}
if (opts.includeSeedData && content.seedData) {
packageContent.seedData = content.seedData
}
zip.file('content.json', JSON.stringify(packageContent, null, 2))
if (opts.includeAssets && assets.length > 0) {
const assetsFolder = zip.folder('assets')
if (assetsFolder) {
for (const asset of assets) {
const typeFolder = assetsFolder.folder(asset.type + 's')
if (typeFolder) {
const fileName = asset.path.split('/').pop() || 'unnamed'
typeFolder.file(fileName, asset.blob)
}
}
const assetManifest = assets.map(asset => ({
originalPath: asset.path,
type: asset.type,
fileName: asset.path.split('/').pop(),
}))
assetsFolder.file('asset-manifest.json', JSON.stringify(assetManifest, null, 2))
}
}
zip.file('README.md', generateReadme(manifest, content))
const blob = await zip.generateAsync({ type: 'blob' })
return blob
}

View File

@@ -0,0 +1,40 @@
import type { PackageManifest, PackageContent } from './types'
export function generateReadme(manifest: PackageManifest, content: PackageContent): string {
return `# ${manifest.name}
Version: ${manifest.version}
Author: ${manifest.author}
Category: ${manifest.category}
## Description
${manifest.description}
## Contents
- **Schemas**: ${content.schemas?.length || 0} data models
- **Pages**: ${content.pages?.length || 0} page configurations
- **Workflows**: ${content.workflows?.length || 0} workflows
- **Lua Scripts**: ${content.luaScripts?.length || 0} scripts
- **Components**: ${Object.keys(content.componentHierarchy || {}).length} component hierarchies
- **CSS Classes**: ${content.cssClasses?.length || 0} CSS categories
- **Dropdown Configs**: ${content.dropdownConfigs?.length || 0} dropdown configurations
## Tags
${manifest.tags.join(', ')}
## Installation
Import this package through the MetaBuilder Package Manager.
## Dependencies
${manifest.dependencies.length > 0 ? manifest.dependencies.join(', ') : 'None'}
---
Generated by MetaBuilder Package Exporter
`
}

View File

@@ -0,0 +1,51 @@
import JSZip from 'jszip'
import type { AssetType, PackageContent, PackageManifest } from './types'
export async function importPackageFromZip(zipFile: File): Promise<{
manifest: PackageManifest
content: PackageContent
assets: Array<{ path: string; blob: Blob; type: AssetType }>
}> {
const zip = await JSZip.loadAsync(zipFile)
const manifestFile = zip.file('manifest.json')
if (!manifestFile) {
throw new Error('Invalid package: manifest.json not found')
}
const manifestText = await manifestFile.async('text')
const manifest: PackageManifest = JSON.parse(manifestText)
const contentFile = zip.file('content.json')
if (!contentFile) {
throw new Error('Invalid package: content.json not found')
}
const contentText = await contentFile.async('text')
const content: PackageContent = JSON.parse(contentText)
const assets: Array<{ path: string; blob: Blob; type: AssetType }> = []
const assetManifestFile = zip.file('assets/asset-manifest.json')
if (assetManifestFile) {
const assetManifestText = await assetManifestFile.async('text')
const assetManifest: Array<{ originalPath: string; type: AssetType; fileName: string }> =
JSON.parse(assetManifestText)
for (const assetInfo of assetManifest) {
const assetPath = `assets/${assetInfo.type}s/${assetInfo.fileName}`
const assetFile = zip.file(assetPath)
if (assetFile) {
const blob = await assetFile.async('blob')
assets.push({
path: assetInfo.originalPath,
blob,
type: assetInfo.type,
})
}
}
}
return { manifest, content, assets }
}

View File

@@ -0,0 +1,24 @@
import type { PackageManifest, PackageContent } from '../package-types'
export interface ExportPackageOptions {
includeAssets?: boolean
includeSchemas?: boolean
includePages?: boolean
includeWorkflows?: boolean
includeLuaScripts?: boolean
includeComponentHierarchy?: boolean
includeComponentConfigs?: boolean
includeCssClasses?: boolean
includeDropdownConfigs?: boolean
includeSeedData?: boolean
}
export type AssetType = 'image' | 'video' | 'audio' | 'document'
export interface AssetFile {
path: string
blob: Blob
type: AssetType
}
export type { PackageManifest, PackageContent }

View File

@@ -0,0 +1,7 @@
export function evaluateConditional(condition: string | boolean, context: Record<string, any>): boolean {
if (typeof condition === 'boolean') return condition
if (!condition) return true
const value = context[condition]
return Boolean(value)
}

View File

@@ -0,0 +1,47 @@
import type { DeclarativeRendererState } from './renderer-state'
export async function executeLuaScript(
state: DeclarativeRendererState,
scriptId: string,
params: any[]
): Promise<any> {
const script = state.luaScripts[scriptId]
if (!script) {
throw new Error(`Lua script not found: ${scriptId}`)
}
const paramContext: Record<string, any> = {}
script.parameters.forEach((param, index) => {
if (params[index] !== undefined) {
paramContext[param.name] = params[index]
}
})
const paramAssignments = script.parameters
.map(p => `local ${p.name} = context.data.params["${p.name}"]`)
.join('\n')
const paramList = script.parameters.map(p => p.name).join(', ')
const wrappedCode = `
${paramAssignments}
${script.code}
local result_fn = sendMessage or handleCommand or formatTime or userJoin or userLeave or countThreads
if result_fn and type(result_fn) == "function" then
return result_fn(${paramList})
end
`
const result = await state.luaEngine.execute(wrappedCode, {
data: { params: paramContext },
})
if (!result.success) {
console.error(`Lua script error (${scriptId}):`, result.error, result.logs)
throw new Error(result.error || 'Lua script execution failed')
}
return result.result
}

View File

@@ -0,0 +1,9 @@
import type { DeclarativeRendererState } from './renderer-state'
import type { DeclarativeComponentConfig } from './types'
export function getComponentConfig(
state: DeclarativeRendererState,
componentType: string
): DeclarativeComponentConfig | undefined {
return state.componentConfigs[componentType]
}

View File

@@ -0,0 +1,5 @@
import type { DeclarativeRendererState } from './renderer-state'
export function hasComponentConfig(state: DeclarativeRendererState, componentType: string): boolean {
return componentType in state.componentConfigs
}

View File

@@ -0,0 +1,8 @@
export function interpolateValue(template: string, context: Record<string, any>): string {
if (!template || typeof template !== 'string') return template
return template.replace(/\{([^}]+)\}/g, (match, key) => {
const value = context[key]
return value !== undefined ? String(value) : match
})
}

View File

@@ -0,0 +1,10 @@
import type { DeclarativeComponentConfig } from './types'
import type { DeclarativeRendererState } from './renderer-state'
export function registerComponentConfig(
state: DeclarativeRendererState,
componentType: string,
config: DeclarativeComponentConfig
): void {
state.componentConfigs[componentType] = config
}

View File

@@ -0,0 +1,10 @@
import type { DeclarativeRendererState } from './renderer-state'
import type { LuaScriptDefinition } from './types'
export function registerLuaScript(
state: DeclarativeRendererState,
scriptId: string,
script: LuaScriptDefinition
): void {
state.luaScripts[scriptId] = script
}

View File

@@ -0,0 +1,16 @@
import { LuaEngine } from '../lua-engine'
import type { DeclarativeComponentConfig, LuaScriptDefinition } from './types'
export type DeclarativeRendererState = {
luaEngine: LuaEngine
componentConfigs: Record<string, DeclarativeComponentConfig>
luaScripts: Record<string, LuaScriptDefinition>
}
export function createDeclarativeRendererState(): DeclarativeRendererState {
return {
luaEngine: new LuaEngine(),
componentConfigs: {},
luaScripts: {},
}
}

View File

@@ -0,0 +1,36 @@
export interface DeclarativeComponentConfig {
type: string
category: string
label: string
description: string
icon: string
props: Array<{
name: string
type: string
label: string
defaultValue?: any
required: boolean
}>
config: {
layout: string
styling: {
className: string
}
children: any[]
}
}
export interface MessageFormat {
id: string
username: string
userId: string
message: string
timestamp: number
type: 'message' | 'system' | 'join' | 'leave'
}
export interface LuaScriptDefinition {
code: string
parameters: any[]
returnType: string
}

View File

@@ -34,7 +34,6 @@ export const ACCESS_RULES: AccessRule[] = [
{ resource: 'smtpConfig', operation: 'READ', allowedRoles: ['god', 'supergod'] },
{ resource: 'smtpConfig', operation: 'UPDATE', allowedRoles: ['supergod'] },
{ resource: 'credential', operation: 'READ', allowedRoles: ['public', 'user', 'admin', 'god', 'supergod'] },
{ resource: 'credential', operation: 'READ', allowedRoles: ['public', 'user', 'admin', 'god', 'supergod'] },
{ resource: 'credential', operation: 'UPDATE', allowedRoles: ['user', 'admin', 'god', 'supergod'] },