From 4c70eef94bb5fbefb77744bfa3f5ca6295434ff2 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Thu, 25 Dec 2025 18:40:14 +0000 Subject: [PATCH] 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. --- .../src/hooks/use-dbal/use-blob-storage.ts | 102 +++++++ .../src/hooks/use-dbal/use-cached-data.ts | 69 +++++ .../nextjs/src/hooks/use-dbal/use-dbal.ts | 29 ++ .../nextjs/src/hooks/use-dbal/use-kv-store.ts | 100 +++++++ .../src/lib/db/users/get-user-by-id.test.ts | 43 ++- .../nextjs/src/lib/packages/package-export.ts | 258 +----------------- .../packages/package-export/download-zip.ts | 10 + .../export-database-snapshot.ts | 45 +++ .../package-export/export-package-as-zip.ts | 94 +++++++ .../package-export/generate-readme.ts | 40 +++ .../package-export/import-package-from-zip.ts | 51 ++++ .../src/lib/packages/package-export/types.ts | 24 ++ .../evaluate-conditional.ts | 7 + .../execute-lua-script.ts | 47 ++++ .../get-component-config.ts | 9 + .../has-component-config.ts | 5 + .../index.ts} | 0 .../interpolate-value.ts | 8 + .../register-component-config.ts | 10 + .../register-lua-script.ts | 10 + .../renderer-state.ts | 16 ++ .../declarative-component-renderer/types.ts | 36 +++ .../lib/security/secure-db/access-rules.ts | 1 - 23 files changed, 759 insertions(+), 255 deletions(-) create mode 100644 frontends/nextjs/src/hooks/use-dbal/use-blob-storage.ts create mode 100644 frontends/nextjs/src/hooks/use-dbal/use-cached-data.ts create mode 100644 frontends/nextjs/src/hooks/use-dbal/use-dbal.ts create mode 100644 frontends/nextjs/src/hooks/use-dbal/use-kv-store.ts create mode 100644 frontends/nextjs/src/lib/packages/package-export/download-zip.ts create mode 100644 frontends/nextjs/src/lib/packages/package-export/export-database-snapshot.ts create mode 100644 frontends/nextjs/src/lib/packages/package-export/export-package-as-zip.ts create mode 100644 frontends/nextjs/src/lib/packages/package-export/generate-readme.ts create mode 100644 frontends/nextjs/src/lib/packages/package-export/import-package-from-zip.ts create mode 100644 frontends/nextjs/src/lib/packages/package-export/types.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/evaluate-conditional.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/execute-lua-script.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/get-component-config.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/has-component-config.ts rename frontends/nextjs/src/lib/rendering/{declarative-component-renderer.ts => declarative-component-renderer/index.ts} (100%) create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/interpolate-value.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-component-config.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-lua-script.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/renderer-state.ts create mode 100644 frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts diff --git a/frontends/nextjs/src/hooks/use-dbal/use-blob-storage.ts b/frontends/nextjs/src/hooks/use-dbal/use-blob-storage.ts new file mode 100644 index 000000000..0d6815055 --- /dev/null +++ b/frontends/nextjs/src/hooks/use-dbal/use-blob-storage.ts @@ -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) => { + 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 => { + 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 => { + 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> => { + 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, + } +} diff --git a/frontends/nextjs/src/hooks/use-dbal/use-cached-data.ts b/frontends/nextjs/src/hooks/use-dbal/use-cached-data.ts new file mode 100644 index 000000000..0bb03db10 --- /dev/null +++ b/frontends/nextjs/src/hooks/use-dbal/use-cached-data.ts @@ -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(key: string, tenantId?: string, userId?: string) { + const kv = useKVStore(tenantId, userId) + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const loadData = async () => { + if (!kv.isReady) return + + try { + setLoading(true) + const cached = await kv.get(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, + } +} diff --git a/frontends/nextjs/src/hooks/use-dbal/use-dbal.ts b/frontends/nextjs/src/hooks/use-dbal/use-dbal.ts new file mode 100644 index 000000000..621d6c532 --- /dev/null +++ b/frontends/nextjs/src/hooks/use-dbal/use-dbal.ts @@ -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(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 } +} diff --git a/frontends/nextjs/src/hooks/use-dbal/use-kv-store.ts b/frontends/nextjs/src/hooks/use-dbal/use-kv-store.ts new file mode 100644 index 000000000..ad09bc45a --- /dev/null +++ b/frontends/nextjs/src/hooks/use-dbal/use-kv-store.ts @@ -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 (key: string): Promise => { + if (!isReady) { + throw new Error('DBAL not ready') + } + try { + return await dbal.kvGet(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 => { + 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 => { + 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, + } +} diff --git a/frontends/nextjs/src/lib/db/users/get-user-by-id.test.ts b/frontends/nextjs/src/lib/db/users/get-user-by-id.test.ts index 2935ef851..2df57eb7a 100644 --- a/frontends/nextjs/src/lib/db/users/get-user-by-id.test.ts +++ b/frontends/nextjs/src/lib/db/users/get-user-by-id.test.ts @@ -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) }) }) diff --git a/frontends/nextjs/src/lib/packages/package-export.ts b/frontends/nextjs/src/lib/packages/package-export.ts index 91bcba70a..d067dd83f 100644 --- a/frontends/nextjs/src/lib/packages/package-export.ts +++ b/frontends/nextjs/src/lib/packages/package-export.ts @@ -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 { - 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 = {} - - 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, - componentConfigs: Record, - cssClasses: any[], - dropdownConfigs: any[], - assets: AssetFile[] = [] -): Promise { - 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' diff --git a/frontends/nextjs/src/lib/packages/package-export/download-zip.ts b/frontends/nextjs/src/lib/packages/package-export/download-zip.ts new file mode 100644 index 000000000..48fe3baac --- /dev/null +++ b/frontends/nextjs/src/lib/packages/package-export/download-zip.ts @@ -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) +} diff --git a/frontends/nextjs/src/lib/packages/package-export/export-database-snapshot.ts b/frontends/nextjs/src/lib/packages/package-export/export-database-snapshot.ts new file mode 100644 index 000000000..9fc541d43 --- /dev/null +++ b/frontends/nextjs/src/lib/packages/package-export/export-database-snapshot.ts @@ -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, + componentConfigs: Record, + cssClasses: any[], + dropdownConfigs: any[], + assets: AssetFile[] = [] +): Promise { + 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) +} diff --git a/frontends/nextjs/src/lib/packages/package-export/export-package-as-zip.ts b/frontends/nextjs/src/lib/packages/package-export/export-package-as-zip.ts new file mode 100644 index 000000000..ec6a7cda0 --- /dev/null +++ b/frontends/nextjs/src/lib/packages/package-export/export-package-as-zip.ts @@ -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 { + 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 = {} + + 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 +} diff --git a/frontends/nextjs/src/lib/packages/package-export/generate-readme.ts b/frontends/nextjs/src/lib/packages/package-export/generate-readme.ts new file mode 100644 index 000000000..24b0a49aa --- /dev/null +++ b/frontends/nextjs/src/lib/packages/package-export/generate-readme.ts @@ -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 +` +} diff --git a/frontends/nextjs/src/lib/packages/package-export/import-package-from-zip.ts b/frontends/nextjs/src/lib/packages/package-export/import-package-from-zip.ts new file mode 100644 index 000000000..b6a1a0453 --- /dev/null +++ b/frontends/nextjs/src/lib/packages/package-export/import-package-from-zip.ts @@ -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 } +} diff --git a/frontends/nextjs/src/lib/packages/package-export/types.ts b/frontends/nextjs/src/lib/packages/package-export/types.ts new file mode 100644 index 000000000..3515575d3 --- /dev/null +++ b/frontends/nextjs/src/lib/packages/package-export/types.ts @@ -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 } diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/evaluate-conditional.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/evaluate-conditional.ts new file mode 100644 index 000000000..9caa2c061 --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/evaluate-conditional.ts @@ -0,0 +1,7 @@ +export function evaluateConditional(condition: string | boolean, context: Record): boolean { + if (typeof condition === 'boolean') return condition + if (!condition) return true + + const value = context[condition] + return Boolean(value) +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/execute-lua-script.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/execute-lua-script.ts new file mode 100644 index 000000000..c3a1d68dc --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/execute-lua-script.ts @@ -0,0 +1,47 @@ +import type { DeclarativeRendererState } from './renderer-state' + +export async function executeLuaScript( + state: DeclarativeRendererState, + scriptId: string, + params: any[] +): Promise { + const script = state.luaScripts[scriptId] + if (!script) { + throw new Error(`Lua script not found: ${scriptId}`) + } + + const paramContext: Record = {} + 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 +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/get-component-config.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/get-component-config.ts new file mode 100644 index 000000000..da613679c --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/get-component-config.ts @@ -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] +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/has-component-config.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/has-component-config.ts new file mode 100644 index 000000000..7f18c1330 --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/has-component-config.ts @@ -0,0 +1,5 @@ +import type { DeclarativeRendererState } from './renderer-state' + +export function hasComponentConfig(state: DeclarativeRendererState, componentType: string): boolean { + return componentType in state.componentConfigs +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/index.ts similarity index 100% rename from frontends/nextjs/src/lib/rendering/declarative-component-renderer.ts rename to frontends/nextjs/src/lib/rendering/declarative-component-renderer/index.ts diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/interpolate-value.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/interpolate-value.ts new file mode 100644 index 000000000..2d2be0d88 --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/interpolate-value.ts @@ -0,0 +1,8 @@ +export function interpolateValue(template: string, context: Record): string { + if (!template || typeof template !== 'string') return template + + return template.replace(/\{([^}]+)\}/g, (match, key) => { + const value = context[key] + return value !== undefined ? String(value) : match + }) +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-component-config.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-component-config.ts new file mode 100644 index 000000000..47a0ca8bd --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-component-config.ts @@ -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 +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-lua-script.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-lua-script.ts new file mode 100644 index 000000000..94da4d41f --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/register-lua-script.ts @@ -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 +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/renderer-state.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/renderer-state.ts new file mode 100644 index 000000000..ab6b63dda --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/renderer-state.ts @@ -0,0 +1,16 @@ +import { LuaEngine } from '../lua-engine' +import type { DeclarativeComponentConfig, LuaScriptDefinition } from './types' + +export type DeclarativeRendererState = { + luaEngine: LuaEngine + componentConfigs: Record + luaScripts: Record +} + +export function createDeclarativeRendererState(): DeclarativeRendererState { + return { + luaEngine: new LuaEngine(), + componentConfigs: {}, + luaScripts: {}, + } +} diff --git a/frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts new file mode 100644 index 000000000..2e2673e57 --- /dev/null +++ b/frontends/nextjs/src/lib/rendering/declarative-component-renderer/types.ts @@ -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 +} diff --git a/frontends/nextjs/src/lib/security/secure-db/access-rules.ts b/frontends/nextjs/src/lib/security/secure-db/access-rules.ts index 2e20ee091..8c1d6a0ec 100644 --- a/frontends/nextjs/src/lib/security/secure-db/access-rules.ts +++ b/frontends/nextjs/src/lib/security/secure-db/access-rules.ts @@ -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'] },