Files
metabuilder/frontends/nextjs/src/lib/database.ts
JohnDoe6345789 78c2d1af50 feat(seed): add seed data module and tests for database initialization
feat(types): introduce builder and level types for component and user roles

feat(workflow): implement workflow engine with execution context and logging

style(theme): create modular theme exports for consistent styling

chore(tests): add comprehensive tests for workflow execution and Lua nodes
2025-12-25 17:33:26 +00:00

1360 lines
44 KiB
TypeScript

import { prisma } from './prisma'
import type {
User,
Workflow,
LuaScript,
PageConfig,
AppConfiguration,
Comment,
Tenant,
PowerTransferRequest,
} from '../types/level-types'
import type { ModelSchema } from './types/schema-types'
import type { InstalledPackage } from './package-types'
import type { SMTPConfig } from './password-utils'
/**
* Database class - provides data access layer
*
* NOTE: DBAL integration is available server-side only.
* For server-side code (API routes, server components), import and use database-dbal.server.ts directly.
* This class currently uses Prisma directly but maintains compatibility with DBAL for server contexts.
*/
export interface CssCategory {
name: string
classes: string[]
}
export interface DropdownConfig {
id: string
name: string
label: string
options: Array<{ value: string; label: string }>
}
export interface DatabaseSchema {
users: User[]
credentials: Record<string, string>
workflows: Workflow[]
luaScripts: LuaScript[]
pages: PageConfig[]
schemas: ModelSchema[]
appConfig: AppConfiguration
comments: Comment[]
componentHierarchy: Record<string, ComponentNode>
componentConfigs: Record<string, ComponentConfig>
godCredentialsExpiry: number
passwordChangeTimestamps: Record<string, number>
firstLoginFlags: Record<string, boolean>
godCredentialsExpiryDuration: number
cssClasses: CssCategory[]
dropdownConfigs: DropdownConfig[]
tenants: Tenant[]
powerTransferRequests: PowerTransferRequest[]
smtpConfig: SMTPConfig
passwordResetTokens: Record<string, string>
}
export interface ComponentNode {
id: string
type: string
parentId?: string
childIds: string[]
order: number
pageId: string
}
export interface ComponentConfig {
id: string
componentId: string
props: Record<string, any>
styles: Record<string, any>
events: Record<string, string>
conditionalRendering?: {
condition: string
luaScriptId?: string
}
}
export const DB_KEYS = {
USERS: 'db_users',
CREDENTIALS: 'db_credentials',
WORKFLOWS: 'db_workflows',
LUA_SCRIPTS: 'db_lua_scripts',
PAGES: 'db_pages',
SCHEMAS: 'db_schemas',
APP_CONFIG: 'db_app_config',
COMMENTS: 'db_comments',
COMPONENT_HIERARCHY: 'db_component_hierarchy',
COMPONENT_CONFIGS: 'db_component_configs',
GOD_CREDENTIALS_EXPIRY: 'db_god_credentials_expiry',
PASSWORD_CHANGE_TIMESTAMPS: 'db_password_change_timestamps',
FIRST_LOGIN_FLAGS: 'db_first_login_flags',
GOD_CREDENTIALS_EXPIRY_DURATION: 'db_god_credentials_expiry_duration',
CSS_CLASSES: 'db_css_classes',
DROPDOWN_CONFIGS: 'db_dropdown_configs',
INSTALLED_PACKAGES: 'db_installed_packages',
PACKAGE_DATA: 'db_package_data',
TENANTS: 'db_tenants',
POWER_TRANSFER_REQUESTS: 'db_power_transfer_requests',
SMTP_CONFIG: 'db_smtp_config',
PASSWORD_RESET_TOKENS: 'db_password_reset_tokens',
} as const
export async function hashPassword(password: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(password)
const hashBuffer = await crypto.subtle.digest('SHA-512', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
return hashHex
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
const inputHash = await hashPassword(password)
return inputHash === hash
}
export class Database {
/**
* Initialize database connection
* Note: DBAL can be initialized separately in server contexts via database-dbal.server.ts
*/
static async initializeDatabase(): Promise<void> {
try {
// Test Prisma connection
await prisma.$connect()
console.log('Database initialized successfully')
} catch (error) {
console.error('Failed to initialize database:', error)
throw error
}
}
// User operations
static async getUsers(): Promise<User[]> {
const users = await prisma.user.findMany()
return users.map(u => ({
id: u.id,
username: u.username,
email: u.email,
role: u.role as any,
profilePicture: u.profilePicture || undefined,
bio: u.bio || undefined,
createdAt: Number(u.createdAt),
tenantId: u.tenantId || undefined,
isInstanceOwner: u.isInstanceOwner,
}))
}
static async setUsers(users: User[]): Promise<void> {
await prisma.user.deleteMany()
for (const user of users) {
await prisma.user.create({
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
profilePicture: user.profilePicture,
bio: user.bio,
createdAt: BigInt(user.createdAt),
tenantId: user.tenantId,
isInstanceOwner: user.isInstanceOwner || false,
passwordChangeTimestamp: null,
firstLogin: false,
},
})
}
}
static async addUser(user: User): Promise<void> {
await prisma.user.create({
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
profilePicture: user.profilePicture,
bio: user.bio,
createdAt: BigInt(user.createdAt),
tenantId: user.tenantId,
isInstanceOwner: user.isInstanceOwner || false,
passwordChangeTimestamp: null,
firstLogin: false,
},
})
}
static async updateUser(userId: string, updates: Partial<User>): Promise<void> {
const data: any = {}
if (updates.username !== undefined) data.username = updates.username
if (updates.email !== undefined) data.email = updates.email
if (updates.role !== undefined) data.role = updates.role
if (updates.profilePicture !== undefined) data.profilePicture = updates.profilePicture
if (updates.bio !== undefined) data.bio = updates.bio
if (updates.tenantId !== undefined) data.tenantId = updates.tenantId
if (updates.isInstanceOwner !== undefined) data.isInstanceOwner = updates.isInstanceOwner
await prisma.user.update({
where: { id: userId },
data,
})
}
static async deleteUser(userId: string): Promise<void> {
await prisma.user.delete({ where: { id: userId } })
}
static async getCredentials(): Promise<Record<string, string>> {
const credentials = await prisma.credential.findMany()
const result: Record<string, string> = {}
for (const cred of credentials) {
result[cred.username] = cred.passwordHash
}
return result
}
static async setCredential(username: string, passwordHash: string): Promise<void> {
await prisma.credential.upsert({
where: { username },
update: { passwordHash },
create: { username, passwordHash },
})
await prisma.user.update({
where: { username },
data: { passwordChangeTimestamp: BigInt(Date.now()) },
})
}
static async getPasswordChangeTimestamps(): Promise<Record<string, number>> {
const users = await prisma.user.findMany({
where: { passwordChangeTimestamp: { not: null } },
select: { username: true, passwordChangeTimestamp: true },
})
const result: Record<string, number> = {}
for (const user of users) {
if (user.passwordChangeTimestamp) {
result[user.username] = Number(user.passwordChangeTimestamp)
}
}
return result
}
static async setPasswordChangeTimestamps(timestamps: Record<string, number>): Promise<void> {
for (const [username, timestamp] of Object.entries(timestamps)) {
await prisma.user.update({
where: { username },
data: { passwordChangeTimestamp: BigInt(timestamp) },
})
}
}
static async verifyCredentials(username: string, password: string): Promise<boolean> {
const credential = await prisma.credential.findUnique({ where: { username } })
if (!credential) return false
return await verifyPassword(password, credential.passwordHash)
}
static async getWorkflows(): Promise<Workflow[]> {
const workflows = await prisma.workflow.findMany()
return workflows.map(w => ({
id: w.id,
name: w.name,
description: w.description || undefined,
nodes: JSON.parse(w.nodes),
edges: JSON.parse(w.edges),
enabled: w.enabled,
}))
}
static async setWorkflows(workflows: Workflow[]): Promise<void> {
await prisma.workflow.deleteMany()
for (const workflow of workflows) {
await prisma.workflow.create({
data: {
id: workflow.id,
name: workflow.name,
description: workflow.description,
nodes: JSON.stringify(workflow.nodes),
edges: JSON.stringify(workflow.edges),
enabled: workflow.enabled,
},
})
}
}
static async addWorkflow(workflow: Workflow): Promise<void> {
await prisma.workflow.create({
data: {
id: workflow.id,
name: workflow.name,
description: workflow.description,
nodes: JSON.stringify(workflow.nodes),
edges: JSON.stringify(workflow.edges),
enabled: workflow.enabled,
},
})
}
static async updateWorkflow(workflowId: string, updates: Partial<Workflow>): Promise<void> {
const data: any = {}
if (updates.name !== undefined) data.name = updates.name
if (updates.description !== undefined) data.description = updates.description
if (updates.nodes !== undefined) data.nodes = JSON.stringify(updates.nodes)
if (updates.edges !== undefined) data.edges = JSON.stringify(updates.edges)
if (updates.enabled !== undefined) data.enabled = updates.enabled
await prisma.workflow.update({
where: { id: workflowId },
data,
})
}
static async deleteWorkflow(workflowId: string): Promise<void> {
await prisma.workflow.delete({ where: { id: workflowId } })
}
static async getLuaScripts(): Promise<LuaScript[]> {
const scripts = await prisma.luaScript.findMany()
return scripts.map(s => ({
id: s.id,
name: s.name,
description: s.description || undefined,
code: s.code,
parameters: JSON.parse(s.parameters),
returnType: s.returnType || undefined,
}))
}
static async setLuaScripts(scripts: LuaScript[]): Promise<void> {
await prisma.luaScript.deleteMany()
for (const script of scripts) {
await prisma.luaScript.create({
data: {
id: script.id,
name: script.name,
description: script.description,
code: script.code,
parameters: JSON.stringify(script.parameters),
returnType: script.returnType,
},
})
}
}
static async addLuaScript(script: LuaScript): Promise<void> {
await prisma.luaScript.create({
data: {
id: script.id,
name: script.name,
description: script.description,
code: script.code,
parameters: JSON.stringify(script.parameters),
returnType: script.returnType,
},
})
}
static async updateLuaScript(scriptId: string, updates: Partial<LuaScript>): Promise<void> {
const data: any = {}
if (updates.name !== undefined) data.name = updates.name
if (updates.description !== undefined) data.description = updates.description
if (updates.code !== undefined) data.code = updates.code
if (updates.parameters !== undefined) data.parameters = JSON.stringify(updates.parameters)
if (updates.returnType !== undefined) data.returnType = updates.returnType
await prisma.luaScript.update({
where: { id: scriptId },
data,
})
}
static async deleteLuaScript(scriptId: string): Promise<void> {
await prisma.luaScript.delete({ where: { id: scriptId } })
}
static async getPages(): Promise<PageConfig[]> {
const pages = await prisma.pageConfig.findMany()
return pages.map(p => ({
id: p.id,
path: p.path,
title: p.title,
level: p.level as any,
componentTree: JSON.parse(p.componentTree),
requiresAuth: p.requiresAuth,
requiredRole: (p.requiredRole as any) || undefined,
}))
}
static async setPages(pages: PageConfig[]): Promise<void> {
await prisma.pageConfig.deleteMany()
for (const page of pages) {
await prisma.pageConfig.create({
data: {
id: page.id,
path: page.path,
title: page.title,
level: page.level,
componentTree: JSON.stringify(page.componentTree),
requiresAuth: page.requiresAuth,
requiredRole: page.requiredRole,
},
})
}
}
static async addPage(page: PageConfig): Promise<void> {
await prisma.pageConfig.create({
data: {
id: page.id,
path: page.path,
title: page.title,
level: page.level,
componentTree: JSON.stringify(page.componentTree),
requiresAuth: page.requiresAuth,
requiredRole: page.requiredRole,
},
})
}
static async updatePage(pageId: string, updates: Partial<PageConfig>): Promise<void> {
const data: any = {}
if (updates.path !== undefined) data.path = updates.path
if (updates.title !== undefined) data.title = updates.title
if (updates.level !== undefined) data.level = updates.level
if (updates.componentTree !== undefined) data.componentTree = JSON.stringify(updates.componentTree)
if (updates.requiresAuth !== undefined) data.requiresAuth = updates.requiresAuth
if (updates.requiredRole !== undefined) data.requiredRole = updates.requiredRole
await prisma.pageConfig.update({
where: { id: pageId },
data,
})
}
static async deletePage(pageId: string): Promise<void> {
await prisma.pageConfig.delete({ where: { id: pageId } })
}
static async getSchemas(): Promise<ModelSchema[]> {
const schemas = await prisma.modelSchema.findMany()
return schemas.map(s => ({
name: s.name,
label: s.label || undefined,
labelPlural: s.labelPlural || undefined,
icon: s.icon || undefined,
fields: JSON.parse(s.fields),
listDisplay: s.listDisplay ? JSON.parse(s.listDisplay) : undefined,
listFilter: s.listFilter ? JSON.parse(s.listFilter) : undefined,
searchFields: s.searchFields ? JSON.parse(s.searchFields) : undefined,
ordering: s.ordering ? JSON.parse(s.ordering) : undefined,
}))
}
static async setSchemas(schemas: ModelSchema[]): Promise<void> {
await prisma.modelSchema.deleteMany()
for (const schema of schemas) {
await prisma.modelSchema.create({
data: {
name: schema.name,
label: schema.label,
labelPlural: schema.labelPlural,
icon: schema.icon,
fields: JSON.stringify(schema.fields),
listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null,
listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null,
searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null,
ordering: schema.ordering ? JSON.stringify(schema.ordering) : null,
},
})
}
}
static async addSchema(schema: ModelSchema): Promise<void> {
await prisma.modelSchema.create({
data: {
name: schema.name,
label: schema.label,
labelPlural: schema.labelPlural,
icon: schema.icon,
fields: JSON.stringify(schema.fields),
listDisplay: schema.listDisplay ? JSON.stringify(schema.listDisplay) : null,
listFilter: schema.listFilter ? JSON.stringify(schema.listFilter) : null,
searchFields: schema.searchFields ? JSON.stringify(schema.searchFields) : null,
ordering: schema.ordering ? JSON.stringify(schema.ordering) : null,
},
})
}
static async updateSchema(schemaName: string, updates: Partial<ModelSchema>): Promise<void> {
const data: any = {}
if (updates.label !== undefined) data.label = updates.label
if (updates.labelPlural !== undefined) data.labelPlural = updates.labelPlural
if (updates.icon !== undefined) data.icon = updates.icon
if (updates.fields !== undefined) data.fields = JSON.stringify(updates.fields)
if (updates.listDisplay !== undefined) data.listDisplay = JSON.stringify(updates.listDisplay)
if (updates.listFilter !== undefined) data.listFilter = JSON.stringify(updates.listFilter)
if (updates.searchFields !== undefined) data.searchFields = JSON.stringify(updates.searchFields)
if (updates.ordering !== undefined) data.ordering = JSON.stringify(updates.ordering)
await prisma.modelSchema.update({
where: { name: schemaName },
data,
})
}
static async deleteSchema(schemaName: string): Promise<void> {
await prisma.modelSchema.delete({ where: { name: schemaName } })
}
static async getAppConfig(): Promise<AppConfiguration | null> {
const config = await prisma.appConfiguration.findFirst()
if (!config) return null
return {
id: config.id,
name: config.name,
schemas: JSON.parse(config.schemas),
workflows: JSON.parse(config.workflows),
luaScripts: JSON.parse(config.luaScripts),
pages: JSON.parse(config.pages),
theme: JSON.parse(config.theme),
}
}
static async setAppConfig(config: AppConfiguration): Promise<void> {
await prisma.appConfiguration.deleteMany()
await prisma.appConfiguration.create({
data: {
id: config.id,
name: config.name,
schemas: JSON.stringify(config.schemas),
workflows: JSON.stringify(config.workflows),
luaScripts: JSON.stringify(config.luaScripts),
pages: JSON.stringify(config.pages),
theme: JSON.stringify(config.theme),
},
})
}
static async getComments(): Promise<Comment[]> {
const comments = await prisma.comment.findMany()
return comments.map(c => ({
id: c.id,
userId: c.userId,
content: c.content,
createdAt: Number(c.createdAt),
updatedAt: c.updatedAt ? Number(c.updatedAt) : undefined,
parentId: c.parentId || undefined,
}))
}
static async setComments(comments: Comment[]): Promise<void> {
await prisma.comment.deleteMany()
for (const comment of comments) {
await prisma.comment.create({
data: {
id: comment.id,
userId: comment.userId,
content: comment.content,
createdAt: BigInt(comment.createdAt),
updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null,
parentId: comment.parentId,
},
})
}
}
static async addComment(comment: Comment): Promise<void> {
await prisma.comment.create({
data: {
id: comment.id,
userId: comment.userId,
content: comment.content,
createdAt: BigInt(comment.createdAt),
updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null,
parentId: comment.parentId,
},
})
}
static async updateComment(commentId: string, updates: Partial<Comment>): Promise<void> {
const data: any = {}
if (updates.content !== undefined) data.content = updates.content
if (updates.updatedAt !== undefined) data.updatedAt = BigInt(updates.updatedAt)
await prisma.comment.update({
where: { id: commentId },
data,
})
}
static async deleteComment(commentId: string): Promise<void> {
await prisma.comment.delete({ where: { id: commentId } })
}
static async getComponentHierarchy(): Promise<Record<string, ComponentNode>> {
const nodes = await prisma.componentNode.findMany()
const result: Record<string, ComponentNode> = {}
for (const node of nodes) {
result[node.id] = {
id: node.id,
type: node.type,
parentId: node.parentId || undefined,
childIds: JSON.parse(node.childIds),
order: node.order,
pageId: node.pageId,
}
}
return result
}
static async setComponentHierarchy(hierarchy: Record<string, ComponentNode>): Promise<void> {
await prisma.componentNode.deleteMany()
for (const node of Object.values(hierarchy)) {
await prisma.componentNode.create({
data: {
id: node.id,
type: node.type,
parentId: node.parentId,
childIds: JSON.stringify(node.childIds),
order: node.order,
pageId: node.pageId,
},
})
}
}
static async addComponentNode(node: ComponentNode): Promise<void> {
await prisma.componentNode.create({
data: {
id: node.id,
type: node.type,
parentId: node.parentId,
childIds: JSON.stringify(node.childIds),
order: node.order,
pageId: node.pageId,
},
})
}
static async updateComponentNode(nodeId: string, updates: Partial<ComponentNode>): Promise<void> {
const data: any = {}
if (updates.type !== undefined) data.type = updates.type
if (updates.parentId !== undefined) data.parentId = updates.parentId
if (updates.childIds !== undefined) data.childIds = JSON.stringify(updates.childIds)
if (updates.order !== undefined) data.order = updates.order
if (updates.pageId !== undefined) data.pageId = updates.pageId
await prisma.componentNode.update({
where: { id: nodeId },
data,
})
}
static async deleteComponentNode(nodeId: string): Promise<void> {
await prisma.componentNode.delete({ where: { id: nodeId } })
}
static async getComponentConfigs(): Promise<Record<string, ComponentConfig>> {
const configs = await prisma.componentConfig.findMany()
const result: Record<string, ComponentConfig> = {}
for (const config of configs) {
result[config.id] = {
id: config.id,
componentId: config.componentId,
props: JSON.parse(config.props),
styles: JSON.parse(config.styles),
events: JSON.parse(config.events),
conditionalRendering: config.conditionalRendering ? JSON.parse(config.conditionalRendering) : undefined,
}
}
return result
}
static async setComponentConfigs(configs: Record<string, ComponentConfig>): Promise<void> {
await prisma.componentConfig.deleteMany()
for (const config of Object.values(configs)) {
await prisma.componentConfig.create({
data: {
id: config.id,
componentId: config.componentId,
props: JSON.stringify(config.props),
styles: JSON.stringify(config.styles),
events: JSON.stringify(config.events),
conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null,
},
})
}
}
static async addComponentConfig(config: ComponentConfig): Promise<void> {
await prisma.componentConfig.create({
data: {
id: config.id,
componentId: config.componentId,
props: JSON.stringify(config.props),
styles: JSON.stringify(config.styles),
events: JSON.stringify(config.events),
conditionalRendering: config.conditionalRendering ? JSON.stringify(config.conditionalRendering) : null,
},
})
}
static async updateComponentConfig(configId: string, updates: Partial<ComponentConfig>): Promise<void> {
const data: any = {}
if (updates.componentId !== undefined) data.componentId = updates.componentId
if (updates.props !== undefined) data.props = JSON.stringify(updates.props)
if (updates.styles !== undefined) data.styles = JSON.stringify(updates.styles)
if (updates.events !== undefined) data.events = JSON.stringify(updates.events)
if (updates.conditionalRendering !== undefined) data.conditionalRendering = JSON.stringify(updates.conditionalRendering)
await prisma.componentConfig.update({
where: { id: configId },
data,
})
}
static async deleteComponentConfig(configId: string): Promise<void> {
await prisma.componentConfig.delete({ where: { id: configId } })
}
static async seedDefaultData(): Promise<void> {
const users = await this.getUsers()
const credentials = await this.getCredentials()
if (users.length === 0) {
const defaultUsers: User[] = [
{
id: 'user_supergod',
username: 'supergod',
email: 'supergod@builder.com',
role: 'supergod',
bio: 'Supreme administrator with multi-tenant control',
createdAt: Date.now(),
isInstanceOwner: true,
},
{
id: 'user_god',
username: 'god',
email: 'god@builder.com',
role: 'god',
bio: 'System architect with full access to all levels',
createdAt: Date.now(),
},
{
id: 'user_admin',
username: 'admin',
email: 'admin@builder.com',
role: 'admin',
bio: 'Administrator with data management access',
createdAt: Date.now(),
},
{
id: 'user_demo',
username: 'demo',
email: 'demo@builder.com',
role: 'user',
bio: 'Demo user account',
createdAt: Date.now(),
},
]
await this.setUsers(defaultUsers)
}
if (Object.keys(credentials).length === 0) {
const { getScrambledPassword } = await import('./auth')
await this.setCredential('supergod', await hashPassword(getScrambledPassword('supergod')))
await this.setCredential('god', await hashPassword(getScrambledPassword('god')))
await this.setCredential('admin', await hashPassword(getScrambledPassword('admin')))
await this.setCredential('demo', await hashPassword(getScrambledPassword('demo')))
await this.setFirstLoginFlag('supergod', true)
await this.setFirstLoginFlag('god', true)
await this.setFirstLoginFlag('admin', false)
await this.setFirstLoginFlag('demo', false)
}
const appConfig = await this.getAppConfig()
if (!appConfig) {
const defaultConfig: AppConfiguration = {
id: 'app_001',
name: 'MetaBuilder App',
schemas: [],
workflows: [],
luaScripts: [],
pages: [],
theme: {
colors: {},
fonts: {},
},
}
await this.setAppConfig(defaultConfig)
}
const cssClasses = await this.getCssClasses()
if (cssClasses.length === 0) {
const defaultCssClasses: CssCategory[] = [
{
name: 'Layout',
classes: ['flex', 'flex-col', 'flex-row', 'grid', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'block', 'inline-block', 'inline', 'hidden'],
},
{
name: 'Spacing',
classes: ['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-6', 'p-8', 'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-6', 'm-8', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-6', 'gap-8'],
},
{
name: 'Sizing',
classes: ['w-full', 'w-1/2', 'w-1/3', 'w-1/4', 'w-auto', 'h-full', 'h-screen', 'h-auto', 'min-h-screen', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-4xl', 'max-w-6xl', 'max-w-7xl'],
},
{
name: 'Typography',
classes: ['text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'text-left', 'text-center', 'text-right', 'uppercase', 'lowercase', 'capitalize'],
},
{
name: 'Colors',
classes: ['text-primary', 'text-secondary', 'text-accent', 'text-muted-foreground', 'bg-primary', 'bg-secondary', 'bg-accent', 'bg-background', 'bg-card', 'bg-muted', 'border-primary', 'border-secondary', 'border-accent', 'border-border'],
},
{
name: 'Borders',
classes: ['border', 'border-2', 'border-4', 'border-t', 'border-b', 'border-l', 'border-r', 'rounded', 'rounded-sm', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-2xl', 'rounded-full'],
},
{
name: 'Effects',
classes: ['shadow', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl', 'hover:shadow-lg', 'opacity-0', 'opacity-50', 'opacity-75', 'opacity-100', 'transition', 'transition-all', 'duration-200', 'duration-300', 'duration-500'],
},
{
name: 'Positioning',
classes: ['relative', 'absolute', 'fixed', 'sticky', 'top-0', 'bottom-0', 'left-0', 'right-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50'],
},
{
name: 'Alignment',
classes: ['items-start', 'items-center', 'items-end', 'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around', 'self-start', 'self-center', 'self-end'],
},
{
name: 'Interactivity',
classes: ['cursor-pointer', 'cursor-default', 'pointer-events-none', 'select-none', 'hover:bg-accent', 'hover:text-accent-foreground', 'active:scale-95', 'disabled:opacity-50'],
},
]
await this.setCssClasses(defaultCssClasses)
}
const dropdowns = await this.getDropdownConfigs()
if (dropdowns.length === 0) {
const defaultDropdowns: DropdownConfig[] = [
{
id: 'dropdown_status',
name: 'status_options',
label: 'Status',
options: [
{ value: 'draft', label: 'Draft' },
{ value: 'published', label: 'Published' },
{ value: 'archived', label: 'Archived' },
],
},
{
id: 'dropdown_priority',
name: 'priority_options',
label: 'Priority',
options: [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'urgent', label: 'Urgent' },
],
},
{
id: 'dropdown_category',
name: 'category_options',
label: 'Category',
options: [
{ value: 'general', label: 'General' },
{ value: 'technical', label: 'Technical' },
{ value: 'business', label: 'Business' },
{ value: 'personal', label: 'Personal' },
],
},
]
await this.setDropdownConfigs(defaultDropdowns)
}
}
static async exportDatabase(): Promise<string> {
const data: Partial<DatabaseSchema> = {
users: await this.getUsers(),
workflows: await this.getWorkflows(),
luaScripts: await this.getLuaScripts(),
pages: await this.getPages(),
schemas: await this.getSchemas(),
appConfig: (await this.getAppConfig()) || undefined,
comments: await this.getComments(),
componentHierarchy: await this.getComponentHierarchy(),
componentConfigs: await this.getComponentConfigs(),
}
return JSON.stringify(data, null, 2)
}
static async importDatabase(jsonData: string): Promise<void> {
try {
const data = JSON.parse(jsonData) as Partial<DatabaseSchema>
if (data.users) await this.setUsers(data.users)
if (data.workflows) await this.setWorkflows(data.workflows)
if (data.luaScripts) await this.setLuaScripts(data.luaScripts)
if (data.pages) await this.setPages(data.pages)
if (data.schemas) await this.setSchemas(data.schemas)
if (data.appConfig) await this.setAppConfig(data.appConfig)
if (data.comments) await this.setComments(data.comments)
if (data.componentHierarchy) await this.setComponentHierarchy(data.componentHierarchy)
if (data.componentConfigs) await this.setComponentConfigs(data.componentConfigs)
} catch (error) {
throw new Error('Failed to import database: Invalid JSON')
}
}
static async getGodCredentialsExpiry(): Promise<number> {
const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry' } })
return config ? Number(config.value) : 0
}
static async setGodCredentialsExpiry(timestamp: number): Promise<void> {
await prisma.systemConfig.upsert({
where: { key: 'god_credentials_expiry' },
update: { value: timestamp.toString() },
create: { key: 'god_credentials_expiry', value: timestamp.toString() },
})
}
static async getFirstLoginFlags(): Promise<Record<string, boolean>> {
const users = await prisma.user.findMany({
select: { username: true, firstLogin: true },
})
const result: Record<string, boolean> = {}
for (const user of users) {
result[user.username] = user.firstLogin
}
return result
}
static async setFirstLoginFlag(username: string, isFirstLogin: boolean): Promise<void> {
await prisma.user.update({
where: { username },
data: { firstLogin: isFirstLogin },
})
}
static async shouldShowGodCredentials(): Promise<boolean> {
const expiry = await this.getGodCredentialsExpiry()
const user = await prisma.user.findUnique({
where: { username: 'god' },
select: { passwordChangeTimestamp: true },
})
const godPasswordChangeTime = user?.passwordChangeTimestamp ? Number(user.passwordChangeTimestamp) : 0
if (expiry === 0) {
const duration = await this.getGodCredentialsExpiryDuration()
const expiryTime = Date.now() + duration
await this.setGodCredentialsExpiry(expiryTime)
return true
}
if (godPasswordChangeTime > expiry) {
return false
}
return Date.now() < expiry
}
static async getGodCredentialsExpiryDuration(): Promise<number> {
const config = await prisma.systemConfig.findUnique({ where: { key: 'god_credentials_expiry_duration' } })
return config ? Number(config.value) : (60 * 60 * 1000)
}
static async setGodCredentialsExpiryDuration(durationMs: number): Promise<void> {
await prisma.systemConfig.upsert({
where: { key: 'god_credentials_expiry_duration' },
update: { value: durationMs.toString() },
create: { key: 'god_credentials_expiry_duration', value: durationMs.toString() },
})
}
static async resetGodCredentialsExpiry(): Promise<void> {
const duration = await this.getGodCredentialsExpiryDuration()
const expiryTime = Date.now() + duration
await this.setGodCredentialsExpiry(expiryTime)
}
static async getCssClasses(): Promise<CssCategory[]> {
const categories = await prisma.cssCategory.findMany()
return categories.map(c => ({
name: c.name,
classes: JSON.parse(c.classes),
}))
}
static async setCssClasses(classes: CssCategory[]): Promise<void> {
await prisma.cssCategory.deleteMany()
for (const category of classes) {
await prisma.cssCategory.create({
data: {
name: category.name,
classes: JSON.stringify(category.classes),
},
})
}
}
static async addCssCategory(category: CssCategory): Promise<void> {
await prisma.cssCategory.create({
data: {
name: category.name,
classes: JSON.stringify(category.classes),
},
})
}
static async updateCssCategory(categoryName: string, classes: string[]): Promise<void> {
await prisma.cssCategory.update({
where: { name: categoryName },
data: { classes: JSON.stringify(classes) },
})
}
static async deleteCssCategory(categoryName: string): Promise<void> {
await prisma.cssCategory.delete({ where: { name: categoryName } })
}
static async getDropdownConfigs(): Promise<DropdownConfig[]> {
const configs = await prisma.dropdownConfig.findMany()
return configs.map(c => ({
id: c.id,
name: c.name,
label: c.label,
options: JSON.parse(c.options),
}))
}
static async setDropdownConfigs(configs: DropdownConfig[]): Promise<void> {
await prisma.dropdownConfig.deleteMany()
for (const config of configs) {
await prisma.dropdownConfig.create({
data: {
id: config.id,
name: config.name,
label: config.label,
options: JSON.stringify(config.options),
},
})
}
}
static async addDropdownConfig(config: DropdownConfig): Promise<void> {
await prisma.dropdownConfig.create({
data: {
id: config.id,
name: config.name,
label: config.label,
options: JSON.stringify(config.options),
},
})
}
static async updateDropdownConfig(id: string, updates: DropdownConfig): Promise<void> {
await prisma.dropdownConfig.update({
where: { id },
data: {
name: updates.name,
label: updates.label,
options: JSON.stringify(updates.options),
},
})
}
static async deleteDropdownConfig(id: string): Promise<void> {
await prisma.dropdownConfig.delete({ where: { id } })
}
static async clearDatabase(): Promise<void> {
await prisma.user.deleteMany()
await prisma.credential.deleteMany()
await prisma.workflow.deleteMany()
await prisma.luaScript.deleteMany()
await prisma.pageConfig.deleteMany()
await prisma.modelSchema.deleteMany()
await prisma.appConfiguration.deleteMany()
await prisma.comment.deleteMany()
await prisma.componentNode.deleteMany()
await prisma.componentConfig.deleteMany()
await prisma.systemConfig.deleteMany()
await prisma.cssCategory.deleteMany()
await prisma.dropdownConfig.deleteMany()
await prisma.installedPackage.deleteMany()
await prisma.packageData.deleteMany()
await prisma.tenant.deleteMany()
await prisma.powerTransferRequest.deleteMany()
await prisma.sMTPConfig.deleteMany()
await prisma.passwordResetToken.deleteMany()
}
static async getInstalledPackages(): Promise<InstalledPackage[]> {
const packages = await prisma.installedPackage.findMany()
return packages.map(p => ({
packageId: p.packageId,
installedAt: Number(p.installedAt),
version: p.version,
enabled: p.enabled,
}))
}
static async setInstalledPackages(packages: InstalledPackage[]): Promise<void> {
await prisma.installedPackage.deleteMany()
for (const pkg of packages) {
await prisma.installedPackage.create({
data: {
packageId: pkg.packageId,
installedAt: BigInt(pkg.installedAt),
version: pkg.version,
enabled: pkg.enabled,
},
})
}
}
static async installPackage(packageData: InstalledPackage): Promise<void> {
const exists = await prisma.installedPackage.findUnique({ where: { packageId: packageData.packageId } })
if (!exists) {
await prisma.installedPackage.create({
data: {
packageId: packageData.packageId,
installedAt: BigInt(packageData.installedAt),
version: packageData.version,
enabled: packageData.enabled,
},
})
}
}
static async uninstallPackage(packageId: string): Promise<void> {
await prisma.installedPackage.delete({ where: { packageId } })
}
static async togglePackageEnabled(packageId: string, enabled: boolean): Promise<void> {
await prisma.installedPackage.update({
where: { packageId },
data: { enabled },
})
}
static async getPackageData(packageId: string): Promise<Record<string, any[]>> {
const pkg = await prisma.packageData.findUnique({ where: { packageId } })
return pkg ? JSON.parse(pkg.data) : {}
}
static async setPackageData(packageId: string, data: Record<string, any[]>): Promise<void> {
await prisma.packageData.upsert({
where: { packageId },
update: { data: JSON.stringify(data) },
create: { packageId, data: JSON.stringify(data) },
})
}
static async deletePackageData(packageId: string): Promise<void> {
await prisma.packageData.delete({ where: { packageId } })
}
static async getTenants(): Promise<Tenant[]> {
const tenants = await prisma.tenant.findMany()
return tenants.map(t => ({
id: t.id,
name: t.name,
ownerId: t.ownerId,
createdAt: Number(t.createdAt),
homepageConfig: t.homepageConfig ? JSON.parse(t.homepageConfig) : undefined,
}))
}
static async setTenants(tenants: Tenant[]): Promise<void> {
await prisma.tenant.deleteMany()
for (const tenant of tenants) {
await prisma.tenant.create({
data: {
id: tenant.id,
name: tenant.name,
ownerId: tenant.ownerId,
createdAt: BigInt(tenant.createdAt),
homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null,
},
})
}
}
static async addTenant(tenant: Tenant): Promise<void> {
await prisma.tenant.create({
data: {
id: tenant.id,
name: tenant.name,
ownerId: tenant.ownerId,
createdAt: BigInt(tenant.createdAt),
homepageConfig: tenant.homepageConfig ? JSON.stringify(tenant.homepageConfig) : null,
},
})
}
static async updateTenant(tenantId: string, updates: Partial<Tenant>): Promise<void> {
const data: any = {}
if (updates.name !== undefined) data.name = updates.name
if (updates.ownerId !== undefined) data.ownerId = updates.ownerId
if (updates.homepageConfig !== undefined) data.homepageConfig = JSON.stringify(updates.homepageConfig)
await prisma.tenant.update({
where: { id: tenantId },
data,
})
}
static async deleteTenant(tenantId: string): Promise<void> {
await prisma.tenant.delete({ where: { id: tenantId } })
}
static async getPowerTransferRequests(): Promise<PowerTransferRequest[]> {
const requests = await prisma.powerTransferRequest.findMany()
return requests.map(r => ({
id: r.id,
fromUserId: r.fromUserId,
toUserId: r.toUserId,
status: r.status as any,
createdAt: Number(r.createdAt),
expiresAt: Number(r.expiresAt),
}))
}
static async setPowerTransferRequests(requests: PowerTransferRequest[]): Promise<void> {
await prisma.powerTransferRequest.deleteMany()
for (const request of requests) {
await prisma.powerTransferRequest.create({
data: {
id: request.id,
fromUserId: request.fromUserId,
toUserId: request.toUserId,
status: request.status,
createdAt: BigInt(request.createdAt),
expiresAt: BigInt(request.expiresAt),
},
})
}
}
static async addPowerTransferRequest(request: PowerTransferRequest): Promise<void> {
await prisma.powerTransferRequest.create({
data: {
id: request.id,
fromUserId: request.fromUserId,
toUserId: request.toUserId,
status: request.status,
createdAt: BigInt(request.createdAt),
expiresAt: BigInt(request.expiresAt),
},
})
}
static async updatePowerTransferRequest(requestId: string, updates: Partial<PowerTransferRequest>): Promise<void> {
const data: any = {}
if (updates.status !== undefined) data.status = updates.status
await prisma.powerTransferRequest.update({
where: { id: requestId },
data,
})
}
static async deletePowerTransferRequest(requestId: string): Promise<void> {
await prisma.powerTransferRequest.delete({ where: { id: requestId } })
}
static async getSuperGod(): Promise<User | null> {
const user = await prisma.user.findFirst({
where: { role: 'supergod' },
})
if (!user) return null
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role as any,
profilePicture: user.profilePicture || undefined,
bio: user.bio || undefined,
createdAt: Number(user.createdAt),
tenantId: user.tenantId || undefined,
isInstanceOwner: user.isInstanceOwner,
}
}
static async transferSuperGodPower(fromUserId: string, toUserId: string): Promise<void> {
await prisma.user.update({
where: { id: fromUserId },
data: { role: 'god', isInstanceOwner: false },
})
await prisma.user.update({
where: { id: toUserId },
data: { role: 'supergod', isInstanceOwner: true },
})
}
static async getSMTPConfig(): Promise<SMTPConfig | null> {
const config = await prisma.sMTPConfig.findFirst()
if (!config) return null
return {
host: config.host,
port: config.port,
secure: config.secure,
username: config.username,
password: config.password,
fromEmail: config.fromEmail,
fromName: config.fromName,
}
}
static async setSMTPConfig(config: SMTPConfig): Promise<void> {
await prisma.sMTPConfig.deleteMany()
await prisma.sMTPConfig.create({
data: {
host: config.host,
port: config.port,
secure: config.secure,
username: config.username,
password: config.password,
fromEmail: config.fromEmail,
fromName: config.fromName,
},
})
}
static async getPasswordResetTokens(): Promise<Record<string, string>> {
const tokens = await prisma.passwordResetToken.findMany()
const result: Record<string, string> = {}
for (const token of tokens) {
result[token.username] = token.token
}
return result
}
static async setPasswordResetToken(username: string, token: string): Promise<void> {
await prisma.passwordResetToken.upsert({
where: { username },
update: { token },
create: { username, token },
})
}
static async deletePasswordResetToken(username: string): Promise<void> {
await prisma.passwordResetToken.delete({ where: { username } })
}
}