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
This commit is contained in:
2025-12-25 17:33:26 +00:00
parent f1ca57d9a0
commit 78c2d1af50
152 changed files with 972 additions and 439 deletions

View File

@@ -11,12 +11,24 @@ export interface AdapterCapabilities {
}
export interface DBALAdapter {
// Core CRUD operations
create(entity: string, data: Record<string, unknown>): Promise<unknown>
read(entity: string, id: string): Promise<unknown | null>
update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown>
delete(entity: string, id: string): Promise<boolean>
list(entity: string, options?: ListOptions): Promise<ListResult<unknown>>
// Extended query operations
findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null>
findByField(entity: string, field: string, value: unknown): Promise<unknown | null>
// Extended mutation operations
upsert(entity: string, uniqueField: string, uniqueValue: unknown, createData: Record<string, unknown>, updateData: Record<string, unknown>): Promise<unknown>
updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown>
deleteByField(entity: string, field: string, value: unknown): Promise<boolean>
deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number>
createMany(entity: string, data: Record<string, unknown>[]): Promise<number>
getCapabilities(): Promise<AdapterCapabilities>
close(): Promise<void>
}

View File

@@ -106,6 +106,108 @@ export class PrismaAdapter implements DBALAdapter {
}
}
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
try {
const model = this.getModel(entity)
const where = filter ? this.buildWhereClause(filter) : undefined
const result = await this.withTimeout(
model.findFirst({ where: where as never })
)
return result
} catch (error) {
throw this.handleError(error, 'findFirst', entity)
}
}
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
try {
const model = this.getModel(entity)
const result = await this.withTimeout(
model.findUnique({ where: { [field]: value } as never })
)
return result
} catch (error) {
throw this.handleError(error, 'findByField', entity)
}
}
async upsert(
entity: string,
uniqueField: string,
uniqueValue: unknown,
createData: Record<string, unknown>,
updateData: Record<string, unknown>
): Promise<unknown> {
try {
const model = this.getModel(entity)
const result = await this.withTimeout(
model.upsert({
where: { [uniqueField]: uniqueValue } as never,
create: createData as never,
update: updateData as never,
})
)
return result
} catch (error) {
throw this.handleError(error, 'upsert', entity)
}
}
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
try {
const model = this.getModel(entity)
const result = await this.withTimeout(
model.update({
where: { [field]: value } as never,
data: data as never,
})
)
return result
} catch (error) {
throw this.handleError(error, 'updateByField', entity)
}
}
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
try {
const model = this.getModel(entity)
await this.withTimeout(
model.delete({ where: { [field]: value } as never })
)
return true
} catch (error) {
if (this.isNotFoundError(error)) {
return false
}
throw this.handleError(error, 'deleteByField', entity)
}
}
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
try {
const model = this.getModel(entity)
const where = filter ? this.buildWhereClause(filter) : undefined
const result = await this.withTimeout(
model.deleteMany({ where: where as never })
)
return result.count
} catch (error) {
throw this.handleError(error, 'deleteMany', entity)
}
}
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
try {
const model = this.getModel(entity)
const result = await this.withTimeout(
model.createMany({ data: data as never })
)
return result.count
} catch (error) {
throw this.handleError(error, 'createMany', entity)
}
}
async getCapabilities(): Promise<AdapterCapabilities> {
return {
transactions: true,

View File

@@ -1,19 +1,35 @@
'use client'
import { forwardRef, TextareaHTMLAttributes } from 'react'
import { InputBase } from '@mui/material'
import { forwardRef } from 'react'
import { InputBase, InputBaseProps } from '@mui/material'
export interface TextareaProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'style'> {
export interface TextareaProps {
error?: boolean
disabled?: boolean
placeholder?: string
value?: string
defaultValue?: string
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
onBlur?: (event: React.FocusEvent<HTMLTextAreaElement>) => void
onFocus?: (event: React.FocusEvent<HTMLTextAreaElement>) => void
name?: string
id?: string
rows?: number
minRows?: number
maxRows?: number
className?: string
required?: boolean
readOnly?: boolean
autoFocus?: boolean
}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ error, ...props }, ref) => {
({ error, minRows = 3, ...props }, ref) => {
return (
<InputBase
inputRef={ref}
multiline
minRows={3}
minRows={minRows}
error={error}
sx={{
width: '100%',

View File

@@ -7,35 +7,50 @@
export {
// Button
Button,
type ButtonProps,
type ButtonVariant,
type ButtonSize,
// Input
Input,
type InputProps,
// Textarea
Textarea,
type TextareaProps,
// Label
Label,
type LabelProps,
// Checkbox
Checkbox,
type CheckboxProps,
// Switch
Switch,
type SwitchProps,
// Badge
Badge,
type BadgeProps,
type BadgeVariant,
// Avatar
Avatar,
AvatarGroup,
AvatarImage,
AvatarFallback,
type AvatarProps,
// Separator
Separator,
type SeparatorProps,
// Skeleton
Skeleton,
SkeletonText,
SkeletonCircular,
SkeletonRectangular,
type SkeletonProps,
// Progress
Progress,
CircularProgress,
type ProgressProps,
// Slider
Slider,
type SliderProps,
// Toggle
Toggle,
type ToggleProps,
type ToggleVariant,
type ToggleSize,
} from './atoms'
// ============================================================================
@@ -49,8 +64,6 @@ export {
CardFooter,
CardTitle,
CardDescription,
CardActions,
CardMedia,
// Dialog
Dialog,
DialogTrigger,
@@ -60,6 +73,8 @@ export {
DialogTitle,
DialogDescription,
DialogClose,
DialogOverlay,
DialogPortal,
// Select
Select,
SelectTrigger,
@@ -69,6 +84,8 @@ export {
SelectLabel,
SelectSeparator,
SelectValue,
SelectScrollDownButton,
SelectScrollUpButton,
// Tabs
Tabs,
TabsList,
@@ -78,10 +95,14 @@ export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
SimpleTooltip,
// Alert
Alert,
AlertTitle,
AlertDescription,
type AlertVariant,
type AlertProps,
// Accordion
Accordion,
AccordionItem,
@@ -101,7 +122,8 @@ export {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
useDropdownMenu,
DropdownMenuShortcut,
DropdownMenuPortal,
// RadioGroup
RadioGroup,
RadioGroupItem,
@@ -110,7 +132,6 @@ export {
PopoverTrigger,
PopoverContent,
PopoverAnchor,
usePopover,
// ToggleGroup
ToggleGroup,
ToggleGroupItem,

View File

@@ -80,7 +80,7 @@ const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
({ children, ...props }, ref) => {
return (
<AccordionSummary
ref={ref as React.Ref<HTMLDivElement>}
ref={ref as unknown as React.Ref<HTMLDivElement>}
expandIcon={<ExpandMoreIcon />}
sx={{
'& .MuiAccordionSummary-content': {

View File

@@ -67,7 +67,7 @@ const DialogClose = forwardRef<HTMLButtonElement, DialogCloseProps>(
({ children, onClick, ...props }, ref) => {
if (children) {
return (
<Box ref={ref as React.Ref<HTMLDivElement>} onClick={onClick} sx={{ display: 'inline-flex' }} {...props}>
<Box ref={ref as unknown as React.Ref<HTMLDivElement>} onClick={onClick} sx={{ display: 'inline-flex' }} {...props}>
{children}
</Box>
)

View File

@@ -21,7 +21,7 @@ interface DropdownMenuTriggerProps {
const DropdownMenuTrigger = forwardRef<HTMLButtonElement, DropdownMenuTriggerProps>(
({ children, asChild, ...props }, ref) => {
return (
<Box ref={ref as React.Ref<HTMLDivElement>} sx={{ display: 'inline-flex' }} {...props}>
<Box ref={ref as unknown as React.Ref<HTMLDivElement>} sx={{ display: 'inline-flex' }} {...props}>
{children}
</Box>
)

View File

@@ -44,10 +44,10 @@ interface TooltipContentProps {
}
const TooltipContent = forwardRef<HTMLDivElement, TooltipContentProps>(
({ children, side = 'top', sideOffset = 4, ...props }, ref) => {
({ children, side = 'top', sideOffset = 4 }, ref) => {
return (
<MuiTooltip
ref={ref}
ref={ref as React.Ref<HTMLDivElement>}
title={children}
placement={side}
arrow
@@ -63,9 +63,8 @@ const TooltipContent = forwardRef<HTMLDivElement, TooltipContentProps>(
},
},
}}
{...props}
>
<span>{props.children}</span>
<span>{children}</span>
</MuiTooltip>
)
}

View File

@@ -14,10 +14,34 @@ import {
Divider,
Typography,
Paper,
Kbd,
} from '@mui/material'
import SearchIcon from '@mui/icons-material/Search'
// Custom Kbd component since MUI doesn't export one
const Kbd = ({ children, ...props }: { children: ReactNode; [key: string]: unknown }) => (
<Box
component="kbd"
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'monospace',
fontSize: '0.75rem',
fontWeight: 500,
px: 0.75,
py: 0.25,
borderRadius: 0.5,
bgcolor: 'action.hover',
border: 1,
borderColor: 'divider',
color: 'text.secondary',
}}
{...props}
>
{children}
</Box>
)
// Types
interface CommandItem {
id: string

View File

@@ -246,7 +246,6 @@ const NavigationLink = forwardRef<HTMLElement, NavigationLinkProps>(
return (
<MenuItem
ref={ref}
onClick={onClick}
disabled={disabled}
selected={active}

View File

@@ -11,7 +11,7 @@ import {
canAccessLevel,
getRoleDisplayName,
} from './auth'
import type { UserRole } from './level-types'
import type { UserRole } from '../types/level-types'
describe('auth', () => {
describe('DEFAULT_USERS', () => {

View File

@@ -1,4 +1,4 @@
import type { UserRole } from '../level-types'
import type { UserRole } from '../types/level-types'
/**
* Role hierarchy mapping roles to their numeric permission levels

View File

@@ -1,4 +1,4 @@
import type { User } from '../level-types'
import type { User } from '../types/level-types'
/**
* Default users created during application initialization

View File

@@ -1,4 +1,4 @@
import type { UserRole } from '../level-types'
import type { UserRole } from '../types/level-types'
/**
* Human-readable display names for user roles

View File

@@ -0,0 +1,3 @@
// Component system exports
export { componentCatalog, getComponentCatalog } from './component-catalog'
export { ComponentRegistry, getComponentRegistry } from './component-registry'

View File

@@ -8,8 +8,8 @@ import type {
Comment,
Tenant,
PowerTransferRequest,
} from './level-types'
import type { ModelSchema } from './schema-types'
} from '../types/level-types'
import type { ModelSchema } from './types/schema-types'
import type { InstalledPackage } from './package-types'
import type { SMTPConfig } from './password-utils'

View File

@@ -1,9 +1,11 @@
import { prisma } from '../prisma'
import type { AppConfiguration } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { AppConfiguration } from '../../types/level-types'
export async function getAppConfig(): Promise<AppConfiguration | null> {
const config = await prisma.appConfiguration.findFirst()
if (!config) return null
const adapter = getAdapter()
const result = await adapter.list('AppConfiguration', { limit: 1 })
if (result.data.length === 0) return null
const config = result.data[0] as any
return {
id: config.id,
name: config.name,

View File

@@ -1,17 +1,23 @@
import { prisma } from '../prisma'
import type { AppConfiguration } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { AppConfiguration } from '../../types/level-types'
export async function 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),
},
const adapter = getAdapter()
// Delete existing configs
const existing = await adapter.list('AppConfiguration')
for (const c of existing.data as any[]) {
await adapter.delete('AppConfiguration', c.id)
}
// Create new config
await adapter.create('AppConfiguration', {
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),
})
}

View File

@@ -1,15 +1,17 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Comment } from '../../types/level-types'
/**
* Add a single comment
*/
export async function 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,
},
const adapter = getAdapter()
await adapter.create('Comment', {
id: comment.id,
userId: comment.userId,
content: comment.content,
createdAt: BigInt(comment.createdAt),
updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null,
parentId: comment.parentId,
})
}

View File

@@ -1,5 +1,9 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Delete a comment by ID
*/
export async function deleteComment(commentId: string): Promise<void> {
await prisma.comment.delete({ where: { id: commentId } })
const adapter = getAdapter()
await adapter.delete('Comment', commentId)
}

View File

@@ -1,9 +1,13 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Comment } from '../../types/level-types'
/**
* Get all comments from database
*/
export async function getComments(): Promise<Comment[]> {
const comments = await prisma.comment.findMany()
return comments.map((c) => ({
const adapter = getAdapter()
const result = await adapter.list('Comment')
return (result.data as any[]).map((c) => ({
id: c.id,
userId: c.userId,
content: c.content,

View File

@@ -1,18 +1,27 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Comment } from '../../types/level-types'
/**
* Set all comments (replaces existing)
*/
export async function setComments(comments: Comment[]): Promise<void> {
await prisma.comment.deleteMany()
const adapter = getAdapter()
// Delete existing comments
const existing = await adapter.list('Comment')
for (const c of existing.data as any[]) {
await adapter.delete('Comment', c.id)
}
// Create new comments
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,
},
await adapter.create('Comment', {
id: comment.id,
userId: comment.userId,
content: comment.content,
createdAt: BigInt(comment.createdAt),
updatedAt: comment.updatedAt ? BigInt(comment.updatedAt) : null,
parentId: comment.parentId,
})
}
}

View File

@@ -1,9 +1,13 @@
import { prisma } from '../prisma'
import type { Comment } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Comment } from '../../types/level-types'
/**
* Update a comment by ID
*/
export async function updateComment(commentId: string, updates: Partial<Comment>): Promise<void> {
const data: any = {}
const adapter = getAdapter()
const data: Record<string, unknown> = {}
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 })
await adapter.update('Comment', commentId, data)
}

View File

@@ -1,15 +1,14 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentConfig } from '../types'
export async function 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,
},
const adapter = getAdapter()
await adapter.create('ComponentConfig', {
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,
})
}

View File

@@ -1,15 +1,14 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentNode } from '../types'
export async function 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,
},
const adapter = getAdapter()
await adapter.create('ComponentNode', {
id: node.id,
type: node.type,
parentId: node.parentId,
childIds: JSON.stringify(node.childIds),
order: node.order,
pageId: node.pageId,
})
}

View File

@@ -1,5 +1,6 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
export async function deleteComponentConfig(configId: string): Promise<void> {
await prisma.componentConfig.delete({ where: { id: configId } })
const adapter = getAdapter()
await adapter.delete('ComponentConfig', configId)
}

View File

@@ -1,5 +1,6 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
export async function deleteComponentNode(nodeId: string): Promise<void> {
await prisma.componentNode.delete({ where: { id: nodeId } })
const adapter = getAdapter()
await adapter.delete('ComponentNode', nodeId)
}

View File

@@ -1,11 +1,12 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentConfig } from '../types'
export async function getComponentConfigs(): Promise<Record<string, ComponentConfig>> {
const configs = await prisma.componentConfig.findMany()
const result: Record<string, ComponentConfig> = {}
for (const config of configs) {
result[config.id] = {
const adapter = getAdapter()
const result = await adapter.list('ComponentConfig')
const configs: Record<string, ComponentConfig> = {}
for (const config of result.data as any[]) {
configs[config.id] = {
id: config.id,
componentId: config.componentId,
props: JSON.parse(config.props),
@@ -14,5 +15,5 @@ export async function getComponentConfigs(): Promise<Record<string, ComponentCon
conditionalRendering: config.conditionalRendering ? JSON.parse(config.conditionalRendering) : undefined,
}
}
return result
return configs
}

View File

@@ -1,11 +1,12 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentNode } from '../types'
export async function getComponentHierarchy(): Promise<Record<string, ComponentNode>> {
const nodes = await prisma.componentNode.findMany()
const result: Record<string, ComponentNode> = {}
for (const node of nodes) {
result[node.id] = {
const adapter = getAdapter()
const result = await adapter.list('ComponentNode')
const hierarchy: Record<string, ComponentNode> = {}
for (const node of result.data as any[]) {
hierarchy[node.id] = {
id: node.id,
type: node.type,
parentId: node.parentId || undefined,
@@ -14,5 +15,5 @@ export async function getComponentHierarchy(): Promise<Record<string, ComponentN
pageId: node.pageId,
}
}
return result
return hierarchy
}

View File

@@ -1,18 +1,24 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentConfig } from '../types'
export async function setComponentConfigs(configs: Record<string, ComponentConfig>): Promise<void> {
await prisma.componentConfig.deleteMany()
const adapter = getAdapter()
// Delete existing configs
const existing = await adapter.list('ComponentConfig')
for (const c of existing.data as any[]) {
await adapter.delete('ComponentConfig', c.id)
}
// Create new configs
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,
},
await adapter.create('ComponentConfig', {
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,
})
}
}

View File

@@ -1,18 +1,24 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentNode } from '../types'
export async function setComponentHierarchy(hierarchy: Record<string, ComponentNode>): Promise<void> {
await prisma.componentNode.deleteMany()
const adapter = getAdapter()
// Delete existing hierarchy
const existing = await adapter.list('ComponentNode')
for (const n of existing.data as any[]) {
await adapter.delete('ComponentNode', n.id)
}
// Create new hierarchy
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,
},
await adapter.create('ComponentNode', {
id: node.id,
type: node.type,
parentId: node.parentId,
childIds: JSON.stringify(node.childIds),
order: node.order,
pageId: node.pageId,
})
}
}

View File

@@ -1,8 +1,9 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentConfig } from '../types'
export async function updateComponentConfig(configId: string, updates: Partial<ComponentConfig>): Promise<void> {
const data: any = {}
const adapter = getAdapter()
const data: Record<string, unknown> = {}
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)
@@ -10,5 +11,5 @@ export async function updateComponentConfig(configId: string, updates: Partial<C
if (updates.conditionalRendering !== undefined) {
data.conditionalRendering = updates.conditionalRendering ? JSON.stringify(updates.conditionalRendering) : null
}
await prisma.componentConfig.update({ where: { id: configId }, data })
await adapter.update('ComponentConfig', configId, data)
}

View File

@@ -1,12 +1,13 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import type { ComponentNode } from '../types'
export async function updateComponentNode(nodeId: string, updates: Partial<ComponentNode>): Promise<void> {
const data: any = {}
const adapter = getAdapter()
const data: Record<string, unknown> = {}
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 })
await adapter.update('ComponentNode', nodeId, data)
}

View File

@@ -1,15 +1,19 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import { getPasswordResetTokens } from './get-password-reset-tokens'
/**
* Delete a password reset token
*/
export async function deletePasswordResetToken(username: string): Promise<void> {
const adapter = getAdapter()
const tokens = await getPasswordResetTokens()
delete tokens[username]
await prisma.keyValue.upsert({
where: { key: 'db_password_reset_tokens' },
update: { value: JSON.stringify(tokens) },
create: { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) },
})
const existing = await adapter.list('KeyValue', { filter: { key: 'db_password_reset_tokens' } })
if (existing.data.length > 0) {
const kv = existing.data[0] as any
await adapter.update('KeyValue', kv.id || kv.key, { value: JSON.stringify(tokens) })
} else {
await adapter.create('KeyValue', { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) })
}
}

View File

@@ -1,13 +1,14 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Get all credentials as a username->passwordHash map
*/
export async function 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
const adapter = getAdapter()
const result = await adapter.list('Credential')
const credentials: Record<string, string> = {}
for (const cred of result.data as any[]) {
credentials[cred.username] = cred.passwordHash
}
return result
return credentials
}

View File

@@ -1,18 +1,16 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Get password change timestamps for all users
*/
export async function 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) {
const adapter = getAdapter()
const result = await adapter.list('User')
const timestamps: Record<string, number> = {}
for (const user of result.data as any[]) {
if (user.passwordChangeTimestamp) {
result[user.username] = Number(user.passwordChangeTimestamp)
timestamps[user.username] = Number(user.passwordChangeTimestamp)
}
}
return result
return timestamps
}

View File

@@ -1,9 +1,12 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Get password reset tokens
*/
export async function getPasswordResetTokens(): Promise<Record<string, string>> {
const kv = await prisma.keyValue.findUnique({ where: { key: 'db_password_reset_tokens' } })
const adapter = getAdapter()
const result = await adapter.list('KeyValue', { filter: { key: 'db_password_reset_tokens' } })
if (result.data.length === 0) return {}
const kv = result.data[0] as any
return kv ? JSON.parse(kv.value) : {}
}

View File

@@ -1,17 +1,27 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Set or update a user's credential
*/
export async function setCredential(username: string, passwordHash: string): Promise<void> {
await prisma.credential.upsert({
where: { username },
update: { passwordHash },
create: { username, passwordHash },
})
const adapter = getAdapter()
// Check if credential exists
const result = await adapter.list('Credential', { filter: { username } })
if (result.data.length > 0) {
// Update existing
const existing = result.data[0] as any
await adapter.update('Credential', existing.id || existing.username, { passwordHash })
} else {
// Create new
await adapter.create('Credential', { username, passwordHash })
}
await prisma.user.update({
where: { username },
data: { passwordChangeTimestamp: BigInt(Date.now()) },
})
// Update password change timestamp on user
const users = await adapter.list('User', { filter: { username } })
if (users.data.length > 0) {
const user = users.data[0] as any
await adapter.update('User', user.id, { passwordChangeTimestamp: BigInt(Date.now()) })
}
}

View File

@@ -1,13 +1,15 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Set password change timestamps for users
*/
export async function setPasswordChangeTimestamps(timestamps: Record<string, number>): Promise<void> {
const adapter = getAdapter()
for (const [username, timestamp] of Object.entries(timestamps)) {
await prisma.user.update({
where: { username },
data: { passwordChangeTimestamp: BigInt(timestamp) },
})
const users = await adapter.list('User', { filter: { username } })
if (users.data.length > 0) {
const user = users.data[0] as any
await adapter.update('User', user.id, { passwordChangeTimestamp: BigInt(timestamp) })
}
}
}

View File

@@ -1,15 +1,19 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
import { getPasswordResetTokens } from './get-password-reset-tokens'
/**
* Set a password reset token for a user
*/
export async function setPasswordResetToken(username: string, token: string): Promise<void> {
const adapter = getAdapter()
const tokens = await getPasswordResetTokens()
tokens[username] = token
await prisma.keyValue.upsert({
where: { key: 'db_password_reset_tokens' },
update: { value: JSON.stringify(tokens) },
create: { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) },
})
const existing = await adapter.list('KeyValue', { filter: { key: 'db_password_reset_tokens' } })
if (existing.data.length > 0) {
const kv = existing.data[0] as any
await adapter.update('KeyValue', kv.id || kv.key, { value: JSON.stringify(tokens) })
} else {
await adapter.create('KeyValue', { key: 'db_password_reset_tokens', value: JSON.stringify(tokens) })
}
}

View File

@@ -1,11 +1,13 @@
import { prisma } from '../prisma'
import { verifyPassword } from '../hash-password'
import { getAdapter } from '../dbal-client'
import { verifyPassword } from '../verify-password'
/**
* Verify username/password combination
*/
export async function verifyCredentials(username: string, password: string): Promise<boolean> {
const credential = await prisma.credential.findUnique({ where: { username } })
if (!credential) return false
const adapter = getAdapter()
const result = await adapter.list('Credential', { filter: { username } })
if (result.data.length === 0) return false
const credential = result.data[0] as any
return await verifyPassword(password, credential.passwordHash)
}

View File

@@ -0,0 +1,142 @@
/**
* DBAL Client Singleton
*
* Provides centralized access to the Database Abstraction Layer.
* All db/ lambda functions should use this instead of importing Prisma directly.
*
* This uses the PrismaClient directly but wraps it in a DBAL-compatible interface,
* providing a migration path to the full DBAL when ready.
*/
import { prisma } from '../prisma'
export interface ListOptions {
filter?: Record<string, unknown>
sort?: Record<string, 'asc' | 'desc'>
page?: number
limit?: number
}
export interface ListResult<T> {
data: T[]
total?: number
page?: number
limit?: number
hasMore?: boolean
}
export interface DBALAdapter {
create(entity: string, data: Record<string, unknown>): Promise<unknown>
read(entity: string, id: string): Promise<unknown | null>
update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown>
delete(entity: string, id: string): Promise<boolean>
list(entity: string, options?: ListOptions): Promise<ListResult<unknown>>
close(): Promise<void>
}
/**
* Get the Prisma model by entity name
*/
const getModel = (entity: string): any => {
const modelName = entity.charAt(0).toLowerCase() + entity.slice(1)
const model = (prisma as any)[modelName]
if (!model) {
throw new Error(`Entity ${entity} not found in Prisma schema`)
}
return model
}
/**
* Build where clause from filter
*/
const buildWhereClause = (filter: Record<string, unknown>): Record<string, unknown> => {
const where: Record<string, unknown> = {}
for (const [key, value] of Object.entries(filter)) {
if (value !== undefined) {
where[key] = value
}
}
return where
}
/**
* DBAL Adapter implementation using Prisma
*/
const prismaAdapter: DBALAdapter = {
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
const model = getModel(entity)
return model.create({ data })
},
async read(entity: string, id: string): Promise<unknown | null> {
const model = getModel(entity)
return model.findUnique({ where: { id } })
},
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
const model = getModel(entity)
// Filter out undefined values
const cleanData: Record<string, unknown> = {}
for (const [key, value] of Object.entries(data)) {
if (value !== undefined) {
cleanData[key] = value
}
}
return model.update({ where: { id }, data: cleanData })
},
async delete(entity: string, id: string): Promise<boolean> {
const model = getModel(entity)
try {
await model.delete({ where: { id } })
return true
} catch {
return false
}
},
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
const model = getModel(entity)
const page = options?.page || 1
const limit = options?.limit || 1000
const skip = (page - 1) * limit
const where = options?.filter ? buildWhereClause(options.filter) : undefined
const orderBy = options?.sort
const [data, total] = await Promise.all([
model.findMany({
where,
orderBy,
skip,
take: limit,
}),
model.count({ where }),
])
return {
data,
total,
page,
limit,
hasMore: skip + limit < total,
}
},
async close(): Promise<void> {
await prisma.$disconnect()
},
}
/**
* Get the DBAL adapter singleton for database operations
*/
export const getAdapter = (): DBALAdapter => {
return prismaAdapter
}
/**
* Close the DBAL adapter connection
*/
export const closeAdapter = async (): Promise<void> => {
await prismaAdapter.close()
}

View File

@@ -1,6 +1,10 @@
// Types
export type { CssCategory, DropdownConfig, DatabaseSchema, ComponentNode, ComponentConfig } from './types'
// DBAL Client
export { getAdapter, closeAdapter } from './dbal-client'
export type { DBALAdapter, ListOptions, ListResult } from './dbal-client'
// Core
export { hashPassword } from './hash-password'
export { verifyPassword } from './verify-password'
@@ -13,6 +17,9 @@ export * from './workflows'
export * from './lua-scripts'
export * from './pages'
export * from './schemas'
export * from './comments'
export * from './app-config'
export * from './components'
// Import all for namespace class
import { initializeDatabase } from './initialize-database'
@@ -24,6 +31,9 @@ import * as workflows from './workflows'
import * as luaScripts from './lua-scripts'
import * as pages from './pages'
import * as schemas from './schemas'
import * as comments from './comments'
import * as appConfig from './app-config'
import * as components from './components'
/**
* Database namespace class - groups all DB operations as static methods
@@ -81,4 +91,27 @@ export class Database {
static addSchema = schemas.addSchema
static updateSchema = schemas.updateSchema
static deleteSchema = schemas.deleteSchema
// Comments
static getComments = comments.getComments
static setComments = comments.setComments
static addComment = comments.addComment
static updateComment = comments.updateComment
static deleteComment = comments.deleteComment
// App Config
static getAppConfig = appConfig.getAppConfig
static setAppConfig = appConfig.setAppConfig
// Components
static getComponentHierarchy = components.getComponentHierarchy
static setComponentHierarchy = components.setComponentHierarchy
static addComponentNode = components.addComponentNode
static updateComponentNode = components.updateComponentNode
static deleteComponentNode = components.deleteComponentNode
static getComponentConfigs = components.getComponentConfigs
static setComponentConfigs = components.setComponentConfigs
static addComponentConfig = components.addComponentConfig
static updateComponentConfig = components.updateComponentConfig
static deleteComponentConfig = components.deleteComponentConfig
}

View File

@@ -1,18 +1,17 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { LuaScript } from '../../types/level-types'
/**
* Add a Lua script
*/
export async function 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,
},
const adapter = getAdapter()
await adapter.create('LuaScript', {
id: script.id,
name: script.name,
description: script.description,
code: script.code,
parameters: JSON.stringify(script.parameters),
returnType: script.returnType,
})
}

View File

@@ -1,8 +1,9 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Delete a Lua script by ID
*/
export async function deleteLuaScript(scriptId: string): Promise<void> {
await prisma.luaScript.delete({ where: { id: scriptId } })
const adapter = getAdapter()
await adapter.delete('LuaScript', scriptId)
}

View File

@@ -1,12 +1,13 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { LuaScript } from '../../types/level-types'
/**
* Get all Lua scripts
*/
export async function getLuaScripts(): Promise<LuaScript[]> {
const scripts = await prisma.luaScript.findMany()
return scripts.map((s) => ({
const adapter = getAdapter()
const result = await adapter.list('LuaScript')
return (result.data as any[]).map((s) => ({
id: s.id,
name: s.name,
description: s.description || undefined,

View File

@@ -1,21 +1,27 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { LuaScript } from '../../types/level-types'
/**
* Set all Lua scripts (replaces existing)
*/
export async function setLuaScripts(scripts: LuaScript[]): Promise<void> {
await prisma.luaScript.deleteMany()
const adapter = getAdapter()
// Delete existing scripts
const existing = await adapter.list('LuaScript')
for (const s of existing.data as any[]) {
await adapter.delete('LuaScript', s.id)
}
// Create new scripts
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,
},
await adapter.create('LuaScript', {
id: script.id,
name: script.name,
description: script.description,
code: script.code,
parameters: JSON.stringify(script.parameters),
returnType: script.returnType,
})
}
}

View File

@@ -1,19 +1,17 @@
import { prisma } from '../prisma'
import type { LuaScript } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { LuaScript } from '../../types/level-types'
/**
* Update a Lua script by ID
*/
export async function updateLuaScript(scriptId: string, updates: Partial<LuaScript>): Promise<void> {
const data: any = {}
const adapter = getAdapter()
const data: Record<string, unknown> = {}
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,
})
await adapter.update('LuaScript', scriptId, data)
}

View File

@@ -1,19 +1,18 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { PageConfig } from '../../types/level-types'
/**
* Add a page
*/
export async function 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,
},
const adapter = getAdapter()
await adapter.create('PageConfig', {
id: page.id,
path: page.path,
title: page.title,
level: page.level,
componentTree: JSON.stringify(page.componentTree),
requiresAuth: page.requiresAuth,
requiredRole: page.requiredRole,
})
}

View File

@@ -1,8 +1,9 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Delete a page by ID
*/
export async function deletePage(pageId: string): Promise<void> {
await prisma.pageConfig.delete({ where: { id: pageId } })
const adapter = getAdapter()
await adapter.delete('PageConfig', pageId)
}

View File

@@ -1,12 +1,13 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { PageConfig } from '../../types/level-types'
/**
* Get all pages
*/
export async function getPages(): Promise<PageConfig[]> {
const pages = await prisma.pageConfig.findMany()
return pages.map((p) => ({
const adapter = getAdapter()
const result = await adapter.list('PageConfig')
return (result.data as any[]).map((p) => ({
id: p.id,
path: p.path,
title: p.title,

View File

@@ -1,22 +1,28 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { PageConfig } from '../../types/level-types'
/**
* Set all pages (replaces existing)
*/
export async function setPages(pages: PageConfig[]): Promise<void> {
await prisma.pageConfig.deleteMany()
const adapter = getAdapter()
// Delete existing pages
const existing = await adapter.list('PageConfig')
for (const p of existing.data as any[]) {
await adapter.delete('PageConfig', p.id)
}
// Create new pages
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,
},
await adapter.create('PageConfig', {
id: page.id,
path: page.path,
title: page.title,
level: page.level,
componentTree: JSON.stringify(page.componentTree),
requiresAuth: page.requiresAuth,
requiredRole: page.requiredRole,
})
}
}

View File

@@ -1,11 +1,12 @@
import { prisma } from '../prisma'
import type { PageConfig } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { PageConfig } from '../../types/level-types'
/**
* Update a page by ID
*/
export async function updatePage(pageId: string, updates: Partial<PageConfig>): Promise<void> {
const data: any = {}
const adapter = getAdapter()
const data: Record<string, unknown> = {}
if (updates.path !== undefined) data.path = updates.path
if (updates.title !== undefined) data.title = updates.title
if (updates.level !== undefined) data.level = updates.level
@@ -13,8 +14,5 @@ export async function updatePage(pageId: string, updates: Partial<PageConfig>):
if (updates.requiresAuth !== undefined) data.requiresAuth = updates.requiresAuth
if (updates.requiredRole !== undefined) data.requiredRole = updates.requiredRole
await prisma.pageConfig.update({
where: { id: pageId },
data,
})
await adapter.update('PageConfig', pageId, data)
}

View File

@@ -1,21 +1,20 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
import { getAdapter } from '../dbal-client'
import type { ModelSchema } from '../../types/schema-types'
/**
* Add a schema
*/
export async function 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,
},
const adapter = getAdapter()
await adapter.create('ModelSchema', {
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,
})
}

View File

@@ -1,8 +1,9 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Delete a schema by name
*/
export async function deleteSchema(schemaName: string): Promise<void> {
await prisma.modelSchema.delete({ where: { name: schemaName } })
const adapter = getAdapter()
await adapter.delete('ModelSchema', schemaName)
}

View File

@@ -1,12 +1,13 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
import { getAdapter } from '../dbal-client'
import type { ModelSchema } from '../../types/schema-types'
/**
* Get all schemas
*/
export async function getSchemas(): Promise<ModelSchema[]> {
const schemas = await prisma.modelSchema.findMany()
return schemas.map((s) => ({
const adapter = getAdapter()
const result = await adapter.list('ModelSchema')
return (result.data as any[]).map((s) => ({
name: s.name,
label: s.label || undefined,
labelPlural: s.labelPlural || undefined,

View File

@@ -1,24 +1,30 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
import { getAdapter } from '../dbal-client'
import type { ModelSchema } from '../../types/schema-types'
/**
* Set all schemas (replaces existing)
*/
export async function setSchemas(schemas: ModelSchema[]): Promise<void> {
await prisma.modelSchema.deleteMany()
const adapter = getAdapter()
// Delete existing schemas
const existing = await adapter.list('ModelSchema')
for (const s of existing.data as any[]) {
await adapter.delete('ModelSchema', s.name)
}
// Create new schemas
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,
},
await adapter.create('ModelSchema', {
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,
})
}
}

View File

@@ -1,11 +1,12 @@
import { prisma } from '../prisma'
import type { ModelSchema } from '../../schema-types'
import { getAdapter } from '../dbal-client'
import type { ModelSchema } from '../../types/schema-types'
/**
* Update a schema by name
*/
export async function updateSchema(schemaName: string, updates: Partial<ModelSchema>): Promise<void> {
const data: any = {}
const adapter = getAdapter()
const data: Record<string, unknown> = {}
if (updates.label !== undefined) data.label = updates.label
if (updates.labelPlural !== undefined) data.labelPlural = updates.labelPlural
if (updates.icon !== undefined) data.icon = updates.icon
@@ -15,8 +16,5 @@ export async function updateSchema(schemaName: string, updates: Partial<ModelSch
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,
})
await adapter.update('ModelSchema', schemaName, data)
}

View File

@@ -47,14 +47,14 @@ export interface ComponentConfig {
* Full database schema type
*/
export interface DatabaseSchema {
users: import('../level-types').User[]
users: import('../types/level-types').User[]
credentials: Record<string, string>
workflows: import('../level-types').Workflow[]
luaScripts: import('../level-types').LuaScript[]
pages: import('../level-types').PageConfig[]
schemas: import('../schema-types').ModelSchema[]
appConfig: import('../level-types').AppConfiguration
comments: import('../level-types').Comment[]
workflows: import('../types/level-types').Workflow[]
luaScripts: import('../types/level-types').LuaScript[]
pages: import('../types/level-types').PageConfig[]
schemas: import('../types/schema-types').ModelSchema[]
appConfig: import('../types/level-types').AppConfiguration
comments: import('../types/level-types').Comment[]
componentHierarchy: Record<string, ComponentNode>
componentConfigs: Record<string, ComponentConfig>
godCredentialsExpiry: number
@@ -63,8 +63,8 @@ export interface DatabaseSchema {
godCredentialsExpiryDuration: number
cssClasses: CssCategory[]
dropdownConfigs: DropdownConfig[]
tenants: import('../level-types').Tenant[]
powerTransferRequests: import('../level-types').PowerTransferRequest[]
tenants: import('../types/level-types').Tenant[]
powerTransferRequests: import('../types/level-types').PowerTransferRequest[]
smtpConfig: import('../password').SMTPConfig
passwordResetTokens: Record<string, string>
}

View File

@@ -1,21 +1,20 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
import { getAdapter } from '../dbal-client'
import type { User } from '../../types/level-types'
/**
* Add a single user
*/
export async function 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,
},
const adapter = getAdapter()
await adapter.create('User', {
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,
})
}

View File

@@ -1,8 +1,9 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Delete a user by ID
*/
export async function deleteUser(userId: string): Promise<void> {
await prisma.user.delete({ where: { id: userId } })
const adapter = getAdapter()
await adapter.delete('User', userId)
}

View File

@@ -1,15 +1,15 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
import { getAdapter } from '../dbal-client'
import type { User } from '../../types/level-types'
/**
* Get the SuperGod user (instance owner)
*/
export async function getSuperGod(): Promise<User | null> {
const user = await prisma.user.findFirst({
where: { isInstanceOwner: true },
})
const adapter = getAdapter()
const result = await adapter.list('User', { filter: { isInstanceOwner: true } })
if (!user) return null
if (result.data.length === 0) return null
const user = result.data[0] as any
return {
id: user.id,

View File

@@ -1,12 +1,13 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
import { getAdapter } from '../dbal-client'
import type { User } from '../../types/level-types'
/**
* Get all users from database
*/
export async function getUsers(): Promise<User[]> {
const users = await prisma.user.findMany()
return users.map((u) => ({
const adapter = getAdapter()
const result = await adapter.list('User')
return (result.data as any[]).map((u) => ({
id: u.id,
username: u.username,
email: u.email,

View File

@@ -1,26 +1,31 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
import { getAdapter } from '../dbal-client'
import type { User } from '../../types/level-types'
/**
* Set all users (replaces existing)
* Note: Uses sequential operations - for atomic transactions use prisma directly
*/
export async function setUsers(users: User[]): Promise<void> {
await prisma.$transaction(async (tx) => {
await tx.user.deleteMany()
for (const user of users) {
await tx.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,
},
})
}
})
const adapter = getAdapter()
// Get existing users and delete them
const existing = await adapter.list('User')
for (const user of existing.data as any[]) {
await adapter.delete('User', user.id)
}
// Create new users
for (const user of users) {
await adapter.create('User', {
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,
})
}
}

View File

@@ -1,17 +1,10 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Transfer SuperGod power from one user to another
*/
export async function transferSuperGodPower(fromUserId: string, toUserId: string): Promise<void> {
await prisma.$transaction([
prisma.user.update({
where: { id: fromUserId },
data: { isInstanceOwner: false, role: 'god' },
}),
prisma.user.update({
where: { id: toUserId },
data: { isInstanceOwner: true, role: 'supergod' },
}),
])
const adapter = getAdapter()
await adapter.update('User', fromUserId, { isInstanceOwner: false, role: 'god' })
await adapter.update('User', toUserId, { isInstanceOwner: true, role: 'supergod' })
}

View File

@@ -1,20 +1,18 @@
import { prisma } from '../prisma'
import type { User } from '../level-types'
import { getAdapter } from '../dbal-client'
import type { User } from '../../types/level-types'
/**
* Update a user by ID
*/
export async function updateUser(userId: string, updates: Partial<User>): Promise<void> {
await prisma.user.update({
where: { id: userId },
data: {
username: updates.username,
email: updates.email,
role: updates.role,
profilePicture: updates.profilePicture,
bio: updates.bio,
tenantId: updates.tenantId,
isInstanceOwner: updates.isInstanceOwner,
},
const adapter = getAdapter()
await adapter.update('User', userId, {
username: updates.username,
email: updates.email,
role: updates.role,
profilePicture: updates.profilePicture,
bio: updates.bio,
tenantId: updates.tenantId,
isInstanceOwner: updates.isInstanceOwner,
})
}

View File

@@ -1,18 +1,17 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Workflow } from '../../types/level-types'
/**
* Add a workflow
*/
export async function 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,
},
const adapter = getAdapter()
await adapter.create('Workflow', {
id: workflow.id,
name: workflow.name,
description: workflow.description,
nodes: JSON.stringify(workflow.nodes),
edges: JSON.stringify(workflow.edges),
enabled: workflow.enabled,
})
}

View File

@@ -1,8 +1,9 @@
import { prisma } from '../prisma'
import { getAdapter } from '../dbal-client'
/**
* Delete a workflow by ID
*/
export async function deleteWorkflow(workflowId: string): Promise<void> {
await prisma.workflow.delete({ where: { id: workflowId } })
const adapter = getAdapter()
await adapter.delete('Workflow', workflowId)
}

View File

@@ -1,12 +1,13 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Workflow } from '../../types/level-types'
/**
* Get all workflows
*/
export async function getWorkflows(): Promise<Workflow[]> {
const workflows = await prisma.workflow.findMany()
return workflows.map((w) => ({
const adapter = getAdapter()
const result = await adapter.list('Workflow')
return (result.data as any[]).map((w) => ({
id: w.id,
name: w.name,
description: w.description || undefined,

View File

@@ -1,21 +1,27 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Workflow } from '../../types/level-types'
/**
* Set all workflows (replaces existing)
*/
export async function setWorkflows(workflows: Workflow[]): Promise<void> {
await prisma.workflow.deleteMany()
const adapter = getAdapter()
// Delete existing workflows
const existing = await adapter.list('Workflow')
for (const w of existing.data as any[]) {
await adapter.delete('Workflow', w.id)
}
// Create new workflows
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,
},
await adapter.create('Workflow', {
id: workflow.id,
name: workflow.name,
description: workflow.description,
nodes: JSON.stringify(workflow.nodes),
edges: JSON.stringify(workflow.edges),
enabled: workflow.enabled,
})
}
}

View File

@@ -1,19 +1,17 @@
import { prisma } from '../prisma'
import type { Workflow } from '../../level-types'
import { getAdapter } from '../dbal-client'
import type { Workflow } from '../../types/level-types'
/**
* Update a workflow by ID
*/
export async function updateWorkflow(workflowId: string, updates: Partial<Workflow>): Promise<void> {
const data: any = {}
const adapter = getAdapter()
const data: Record<string, unknown> = {}
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,
})
await adapter.update('Workflow', workflowId, data)
}

View File

@@ -7,7 +7,7 @@ import 'server-only'
import { DBALClient } from '@/lib/dbal-stub'
import type { DBALConfig } from '@/lib/dbal-stub'
import type { User } from './level-types'
import type { User } from '../types/level-types'
let dbalClient: DBALClient | null = null
let initialized = false

View File

@@ -1,6 +1,6 @@
import { DBALClient } from '@/lib/dbal-stub'
import type { DBALConfig } from '@/lib/dbal-stub'
import type { User } from './level-types'
import type { User } from '../types/level-types'
let dbalInstance: DBALClient | null = null

View File

@@ -0,0 +1,4 @@
// DBAL (Database Abstraction Layer) exports
export { DBALClient, createDBALClient } from './dbal-client'
export { getDBALIntegration, initializeDBAL } from './dbal-integration'
export { createDBALStub, DBALStub } from './dbal-stub'

View File

@@ -0,0 +1,51 @@
/**
* Library exports - Centralized re-exports for all lib modules
*
* This file provides a clean API for importing from @/lib
* Instead of: import { User } from '@/lib/types/level-types'
* Use: import { User } from '@/lib'
*/
// Types
export * from './types'
// Core utilities
export * from './utils'
export { prisma } from './prisma'
// Authentication
export * from './auth'
// Database
export { Database } from './database'
export * from './db'
// DBAL
export * from './dbal'
// Schema utilities
export * from './schema'
// Security
export * from './security'
// Lua engine
export * from './lua'
// Components
export * from './components'
// Packages
export * from './packages'
// Rendering
export * from './rendering'
// Seed data
export * from './seed'
// Workflow
export * from './workflow'
// Password utilities
export * from './password'

View File

@@ -0,0 +1,5 @@
// Lua engine exports
export { LuaEngine } from './lua-engine'
export { SandboxedLuaEngine } from './sandboxed-lua-engine'
export { luaSnippets } from './lua-snippets'
export { luaExamples } from './lua-examples'

View File

@@ -0,0 +1,6 @@
// Package system exports
export * from './package-types'
export { initializePackageSystem, buildPackageRegistry } from './package-loader'
export { exportAllPackagesForSeed } from './package-export'
export { packageCatalog } from './package-catalog'
export { packageGlue, getPackageGlue } from './package-glue'

Some files were not shown because too many files have changed in this diff Show More