Merge branch 'main' into dependabot/npm_and_yarn/vitejs/plugin-react-5.1.2

This commit is contained in:
2026-01-06 13:48:40 +00:00
committed by GitHub
77 changed files with 544 additions and 121 deletions

View File

@@ -25,11 +25,12 @@ export const findByField = (context: ACLContext) => async (entity: string, field
export const upsert = (context: ACLContext) => async (
entity: string,
filter: Record<string, unknown>,
uniqueField: string,
uniqueValue: unknown,
createData: Record<string, unknown>,
updateData: Record<string, unknown>,
) => {
return withAudit(context, entity, 'upsert', () => context.baseAdapter.upsert(entity, filter, createData, updateData))
return withAudit(context, entity, 'upsert', () => context.baseAdapter.upsert(entity, uniqueField, uniqueValue, createData, updateData))
}
export const updateByField = (context: ACLContext) => async (

View File

@@ -3,6 +3,8 @@ import type { ACLAdapterOptions, ACLContext, ACLRule, User } from './types'
import { logAudit } from '../acl/audit-logger'
import { defaultACLRules } from '../acl/default-rules'
export type { ACLContext } from './types'
export const createContext = (
baseAdapter: DBALAdapter,
user: User,

View File

@@ -6,9 +6,13 @@ export function createPrismaContext(
options?: PrismaAdapterOptions
): PrismaContext {
const inferredDialect = options?.dialect ?? inferDialectFromUrl(databaseUrl)
const prisma = new PrismaClient({
datasources: databaseUrl ? { db: { url: databaseUrl } } : undefined,
})
const prisma = new PrismaClient(
databaseUrl
? {
datasources: { db: { url: databaseUrl } },
} as any
: undefined
)
return {
prisma,

View File

@@ -12,11 +12,12 @@ type PrismaModelDelegate = {
delete: (...args: unknown[]) => Promise<unknown>
deleteMany: (...args: unknown[]) => Promise<{ count: number }>
upsert: (...args: unknown[]) => Promise<unknown>
count: (...args: unknown[]) => Promise<number>
}
export function getModel(context: PrismaContext, entity: string): PrismaModelDelegate {
const modelName = entity.charAt(0).toLowerCase() + entity.slice(1)
const model = (context.prisma as Record<string, PrismaModelDelegate>)[modelName]
const model = (context.prisma as unknown as Record<string, PrismaModelDelegate>)[modelName]
if (!model) {
throw DBALError.notFound(`Entity ${entity} not found`)

View File

@@ -17,11 +17,16 @@ export async function downloadBuffer(
Range: buildRangeHeader(options),
})
const response = await context.s3Client.send(command)
const response = await context.s3Client.send(command) as {
Body?: AsyncIterable<Uint8Array>
}
const chunks: Uint8Array[] = []
for await (const chunk of response.Body as any) {
chunks.push(chunk)
const body = response.Body
if (body) {
for await (const chunk of body) {
chunks.push(chunk)
}
}
return Buffer.concat(chunks)

View File

@@ -16,9 +16,20 @@ export async function listBlobs(
MaxKeys: options.maxKeys || 1000,
})
const response = await context.s3Client.send(command)
const response = await context.s3Client.send(command) as {
Contents?: Array<{
Key?: string
Size?: number
ETag?: string
LastModified?: Date
}>
NextContinuationToken?: string
IsTruncated?: boolean
}
const items: BlobMetadata[] = (response.Contents || []).map(obj => ({
const contents = response.Contents
const items: BlobMetadata[] = (contents || []).map(obj => ({
key: obj.Key || '',
size: obj.Size || 0,
contentType: 'application/octet-stream',

View File

@@ -14,7 +14,13 @@ export async function getMetadata(
Key: key,
})
const response = await context.s3Client.send(command)
const response = await context.s3Client.send(command) as {
ContentLength?: number
ContentType?: string
ETag?: string
LastModified?: Date
Metadata?: Record<string, string>
}
return {
key,

View File

@@ -19,7 +19,9 @@ export async function uploadBuffer(
Metadata: options.metadata,
})
const response = await context.s3Client.send(command)
const response = await context.s3Client.send(command) as {
ETag?: string
}
return {
key,

View File

@@ -3,7 +3,11 @@ import type { TenantContext } from '../../../core/foundation/tenant-context'
import type { TenantAwareDeps } from './context'
export const resolveTenantContext = async ({ tenantManager, tenantId, userId }: TenantAwareDeps): Promise<TenantContext> => {
return tenantManager.getTenantContext(tenantId, userId)
const hasAccess = await tenantManager.validateTenantAccess(tenantId, userId)
if (!hasAccess) {
throw DBALError.forbidden(`User ${userId} does not have access to tenant ${tenantId}`)
}
return tenantManager.getTenantContext(tenantId)
}
export const ensurePermission = (context: TenantContext, action: 'read' | 'write' | 'delete'): void => {

View File

@@ -1,5 +1,5 @@
import type { DBALAdapter, AdapterCapabilities } from '../../adapters/adapter'
import type { ListOptions, ListResult } from '../../core/types'
import type { ListOptions, ListResult } from '../../core/foundation/types'
import { createConnectionManager } from './connection-manager'
import { createMessageRouter } from './message-router'
import { createOperations } from './operations'
@@ -47,10 +47,13 @@ export class WebSocketBridge implements DBALAdapter {
upsert(
entity: string,
filter: Record<string, unknown>,
uniqueField: string,
uniqueValue: unknown,
createData: Record<string, unknown>,
updateData: Record<string, unknown>,
): Promise<unknown> {
// Convert the new signature to the old one for compatibility
const filter = { [uniqueField]: uniqueValue }
return this.operations.upsert(entity, filter, createData, updateData)
}

View File

@@ -56,7 +56,7 @@ export const createMessageRouter = (state: BridgeState): MessageRouter => ({
state.pendingRequests.delete(response.id)
if (response.error) {
const error = new DBALError(response.error.message, response.error.code, response.error.details)
const error = new DBALError(response.error.code, response.error.message, response.error.details)
pending.reject(error)
} else {
pending.resolve(response.result)

View File

@@ -1,5 +1,5 @@
import type { AdapterCapabilities } from '../../adapters/adapter'
import type { ListOptions, ListResult } from '../../core/types'
import type { ListOptions, ListResult } from '../../core/foundation/types'
import type { ConnectionManager } from './connection-manager'
import type { BridgeState } from './state'
import { rpcCall } from './rpc'

View File

@@ -10,6 +10,8 @@ export interface CreateLuaScriptInput {
isActive?: boolean;
isSandboxed?: boolean;
timeoutMs?: number;
allowedGlobals?: string[];
createdBy?: string;
}
export interface UpdateLuaScriptInput {
@@ -19,6 +21,8 @@ export interface UpdateLuaScriptInput {
isActive?: boolean;
isSandboxed?: boolean;
timeoutMs?: number;
allowedGlobals?: string[];
createdBy?: string;
}
export interface LuaScript {
@@ -26,9 +30,11 @@ export interface LuaScript {
name: string;
code: string;
description?: string;
isActive: boolean;
isActive?: boolean;
isSandboxed: boolean;
timeoutMs: number;
allowedGlobals?: string[];
createdBy?: string;
createdAt: Date;
updatedAt: Date;
}

View File

@@ -4,11 +4,16 @@
*/
export interface CreatePackageInput {
packageId: string;
packageId?: string;
name: string;
version?: string;
description?: string;
isPublished?: boolean;
author?: string;
manifest?: any;
isInstalled?: boolean;
installedAt?: Date;
installedBy?: string;
}
export interface UpdatePackageInput {
@@ -16,15 +21,25 @@ export interface UpdatePackageInput {
version?: string;
description?: string;
isPublished?: boolean;
author?: string;
manifest?: any;
isInstalled?: boolean;
installedAt?: Date;
installedBy?: string;
}
export interface Package {
id: string;
packageId: string;
packageId?: string;
name: string;
version?: string;
description?: string;
isPublished: boolean;
isPublished?: boolean;
author?: string;
manifest?: any;
isInstalled?: boolean;
installedAt?: Date;
installedBy?: string;
createdAt: Date;
updatedAt: Date;
}
@@ -37,3 +52,22 @@ export interface Result<T> {
message: string;
};
}
export interface ListOptions {
filter?: Record<string, any>;
sort?: Record<string, 'asc' | 'desc'>;
page?: number;
limit?: number;
skip?: number;
take?: number;
where?: Record<string, any>;
orderBy?: Record<string, 'asc' | 'desc'>;
}
export interface ListResult<T> {
items?: T[];
data?: T[];
total: number;
skip?: number;
take?: number;
}

View File

@@ -4,7 +4,7 @@
*/
import type { ListOptions, Result, Session } from '../types'
import type { InMemoryStore } from '../store/in-memory-store'
import { cleanExpiredSessions } from './clean-expired'
import { cleanExpiredSessions } from '../lifecycle/clean-expired'
/**
* List sessions with filtering and pagination

View File

@@ -2,8 +2,8 @@
* @file clean-expired.ts
* @description Clean expired sessions operation
*/
import type { Result } from '../../types'
import type { InMemoryStore } from '../../store/in-memory-store'
import type { Result } from '../types'
import type { InMemoryStore } from '../store/in-memory-store'
/**
* Clean up expired sessions

View File

@@ -2,9 +2,9 @@
* @file extend-session.ts
* @description Extend session expiration operation
*/
import type { Result, Session } from '../../types'
import type { InMemoryStore } from '../../store/in-memory-store'
import { validateId } from '../../validation/validate-id'
import type { Result, Session } from '../types'
import type { InMemoryStore } from '../store/in-memory-store'
import { validateId } from '../validation/validate-id'
/**
* Extend a session's expiration time

View File

@@ -6,5 +6,6 @@
export interface InMemoryStore {
sessions: Map<string, any>;
sessionTokens: Map<string, string>;
users: Map<string, any>;
generateId(entityType: string): string;
}

View File

@@ -7,12 +7,16 @@ export interface CreateSessionInput {
userId: string;
token: string;
expiresAt?: Date;
isActive?: boolean;
lastActivity?: Date;
}
export interface UpdateSessionInput {
userId?: string;
token?: string;
expiresAt?: Date;
isActive?: boolean;
lastActivity?: Date;
}
export interface Session {
@@ -20,8 +24,10 @@ export interface Session {
token: string;
userId: string;
expiresAt?: Date;
isActive?: boolean;
lastActivity?: Date;
createdAt: Date;
updatedAt: Date;
updatedAt?: Date;
}
export interface Result<T> {
@@ -32,3 +38,22 @@ export interface Result<T> {
message: string;
};
}
export interface ListOptions {
filter?: Record<string, any>;
sort?: Record<string, 'asc' | 'desc'>;
page?: number;
limit?: number;
skip?: number;
take?: number;
where?: Record<string, any>;
orderBy?: Record<string, 'asc' | 'desc'>;
}
export interface ListResult<T> {
items?: T[];
data?: T[];
total: number;
skip?: number;
take?: number;
}

View File

@@ -4,7 +4,7 @@
*/
import type { CreateUserInput, Result, User } from '../types'
import type { InMemoryStore } from '../store/in-memory-store'
import { validateUserCreate } from '../../validation/validate-user-create'
import { validateUserCreate } from '../validation/validate-user-create'
/**
* Create a new user in the store

View File

@@ -5,7 +5,7 @@
import type { Result, UpdateUserInput, User } from '../types'
import type { InMemoryStore } from '../store/in-memory-store'
import { validateId } from '../validation/validate-id'
import { validateUserUpdate } from '../../validation/validate-user-update'
import { validateUserUpdate } from '../validation/validate-user-update'
/**
* Update an existing user

View File

@@ -8,6 +8,10 @@ export interface CreateUserInput {
email: string;
password?: string;
isActive?: boolean;
role?: 'user' | 'admin' | 'god' | 'supergod';
firstName?: string;
lastName?: string;
displayName?: string;
}
export interface UpdateUserInput {
@@ -15,13 +19,21 @@ export interface UpdateUserInput {
email?: string;
password?: string;
isActive?: boolean;
role?: 'user' | 'admin' | 'god' | 'supergod';
firstName?: string;
lastName?: string;
displayName?: string;
}
export interface User {
id: string;
username: string;
email: string;
isActive: boolean;
isActive?: boolean;
role?: 'user' | 'admin' | 'god' | 'supergod';
firstName?: string;
lastName?: string;
displayName?: string;
createdAt: Date;
updatedAt: Date;
}
@@ -43,3 +55,22 @@ export interface Result<T> {
message: string;
};
}
export interface ListOptions {
filter?: Record<string, any>;
sort?: Record<string, 'asc' | 'desc'>;
page?: number;
limit?: number;
skip?: number;
take?: number;
where?: Record<string, any>;
orderBy?: Record<string, 'asc' | 'desc'>;
}
export interface ListResult<T> {
items?: T[];
data?: T[];
total: number;
skip?: number;
take?: number;
}

View File

@@ -2,7 +2,7 @@
* @file package-validation.ts
* @description Package validation functions
*/
import { isValidSemver } from '../../validation/is-valid-semver'
import { isValidSemver } from '../../../validation/is-valid-semver'
const PACKAGE_ID_REGEX = /^[a-z0-9_]+$/

View File

@@ -2,6 +2,6 @@
* @file page-validation.ts
* @description Page validation functions
*/
import { isValidSlug } from '../../validation/is-valid-slug'
import { isValidSlug } from '../../../validation/is-valid-slug'
export const validateSlug = (slug: string): boolean => isValidSlug(slug)

View File

@@ -2,8 +2,8 @@
* @file user-validation.ts
* @description User validation functions
*/
import { isValidEmail } from '../../validation/is-valid-email'
import { isValidUsername } from '../../validation/is-valid-username'
import { isValidEmail } from '../../../validation/is-valid-email'
import { isValidUsername } from '../../../validation/is-valid-username'
export const validateEmail = (email: string): boolean => isValidEmail(email)
export const validateUsername = (username: string): boolean => isValidUsername(username)

View File

@@ -4,7 +4,7 @@
*/
import type { CreateWorkflowInput, Result, Workflow } from '../types'
import type { InMemoryStore } from '../store/in-memory-store'
import { validateWorkflowCreate } from '../../validation/validate-workflow-create'
import { validateWorkflowCreate } from '../validation/validate-workflow-create'
/**
* Create a new workflow in the store

View File

@@ -5,7 +5,7 @@
import type { Result, UpdateWorkflowInput, Workflow } from '../types'
import type { InMemoryStore } from '../store/in-memory-store'
import { validateId } from '../validation/validate-id'
import { validateWorkflowUpdate } from '../../validation/validate-workflow-update'
import { validateWorkflowUpdate } from '../validation/validate-workflow-update'
/**
* Update an existing workflow

View File

@@ -10,6 +10,8 @@ export interface CreateWorkflowInput {
isActive?: boolean;
trigger?: string;
triggerConfig?: any;
steps?: any[];
createdBy?: string;
}
export interface UpdateWorkflowInput {
@@ -19,6 +21,8 @@ export interface UpdateWorkflowInput {
isActive?: boolean;
trigger?: string;
triggerConfig?: any;
steps?: any[];
createdBy?: string;
}
export interface Workflow {
@@ -26,9 +30,11 @@ export interface Workflow {
name: string;
description?: string;
definition?: any;
isActive: boolean;
isActive?: boolean;
trigger?: string;
triggerConfig?: any;
steps?: any[];
createdBy?: string;
createdAt: Date;
updatedAt: Date;
}
@@ -51,3 +57,22 @@ export interface Result<T> {
message: string;
};
}
export interface ListOptions {
filter?: Record<string, any>;
sort?: Record<string, 'asc' | 'desc'>;
page?: number;
limit?: number;
skip?: number;
take?: number;
where?: Record<string, any>;
orderBy?: Record<string, 'asc' | 'desc'>;
}
export interface ListResult<T> {
items?: T[];
data?: T[];
total: number;
skip?: number;
take?: number;
}

View File

@@ -33,3 +33,10 @@ export interface TenantContext {
canCreateRecord(): boolean
canAddToList(additionalItems: number): boolean
}
export interface TenantManager {
getTenantContext(tenantId: string): Promise<TenantContext>
updateQuota(tenantId: string, quota: Partial<TenantQuota>): Promise<void>
validateTenantAccess(tenantId: string, userId: string): Promise<boolean>
updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void>
}

View File

@@ -12,6 +12,7 @@ export type User = any;
export type Credential = any;
export type Session = any;
export type Page = any;
export type PageView = any;
export type ComponentHierarchy = any;
export type Workflow = any;
export type LuaScript = any;

View File

@@ -1,6 +1,6 @@
import React from 'react'
export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'color'> {
export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'color' | 'content'> {
children?: React.ReactNode
content?: React.ReactNode
dot?: boolean

View File

@@ -46,7 +46,7 @@ export const ButtonBase = forwardRef<HTMLElement, ButtonBaseProps>(function Butt
)
})
export interface InputBaseProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
export interface InputBaseProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue' | 'onFocus' | 'onBlur'> {
disabled?: boolean
error?: boolean
fullWidth?: boolean

View File

@@ -33,7 +33,7 @@ export const TextField = forwardRef<HTMLInputElement | HTMLSelectElement, TextFi
id={id}
error={error}
className="select--full-width"
{...(props as React.SelectHTMLAttributes<HTMLSelectElement>)}
{...(props as unknown as React.SelectHTMLAttributes<HTMLSelectElement>)}
>
{children}
</Select>

View File

@@ -1,6 +1,6 @@
import React, { forwardRef } from 'react'
export interface BottomNavigationProps extends React.HTMLAttributes<HTMLElement> {
export interface BottomNavigationProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
children?: React.ReactNode
value?: any
onChange?: (event: React.SyntheticEvent, value: any) => void

View File

@@ -7,7 +7,7 @@ export interface PaginationRenderItemParams {
disabled: boolean
}
export interface PaginationProps extends Omit<React.HTMLAttributes<HTMLElement>, 'color'> {
export interface PaginationProps extends Omit<React.HTMLAttributes<HTMLElement>, 'color' | 'onChange'> {
count?: number
page?: number
onChange?: (page: number) => void

View File

@@ -80,7 +80,7 @@ export function SpeedDial({
)
}
export interface SpeedDialActionProps extends React.HTMLAttributes<HTMLDivElement> {
export interface SpeedDialActionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> {
icon?: React.ReactNode
tooltipTitle?: string
tooltipOpen?: boolean

View File

@@ -1,6 +1,6 @@
import React, { forwardRef } from 'react'
export interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
children?: React.ReactNode
value?: any
onChange?: (event: React.SyntheticEvent, value: any) => void

View File

@@ -26,7 +26,7 @@
"autoprefixer": "10.4.23",
"eslint": "9.39.2",
"eslint-config-next": "16.1.1",
"postcss": "8.4.35",
"postcss": "8.5.6",
"tailwindcss": "4.1.18",
"typescript": "~5.9.3"
}

View File

@@ -25,20 +25,24 @@ interface TenantLayoutProps {
/**
* Load package dependencies recursively (1 level deep for now)
*/
function getPackageDependencies(packageId: string): { id: string; name?: string }[] {
const metadata = loadPackageMetadata(packageId)
async function getPackageDependencies(packageId: string): Promise<{ id: string; name?: string }[]> {
const metadata = await loadPackageMetadata(packageId) as { dependencies?: string[]; name?: string; minLevel?: number } | null
if (!metadata?.dependencies) {
return []
}
return metadata.dependencies.map(depId => {
const depMetadata = loadPackageMetadata(depId)
return {
id: depId,
name: depMetadata?.name,
minLevel: depMetadata?.minLevel,
}
})
const deps = await Promise.all(
metadata.dependencies.map(async depId => {
const depMetadata = await loadPackageMetadata(depId) as { name?: string; minLevel?: number } | null
return {
id: depId,
name: depMetadata?.name,
minLevel: depMetadata?.minLevel,
}
})
)
return deps
}
export default async function TenantLayout({
@@ -62,7 +66,7 @@ export default async function TenantLayout({
}
// Load dependencies that this page can also use
const additionalPackages = getPackageDependencies(pkg)
const additionalPackages = await getPackageDependencies(pkg)
// TODO: Validate tenant exists against database
// const tenantData = await getTenant(tenant)

View File

@@ -12,6 +12,8 @@ interface RouteParams {
}
}
export const dynamic = 'force-dynamic'
export const GET = async (request: NextRequest, { params }: RouteParams) => {
const runId = Number(params.runId)
if (!Number.isFinite(runId) || runId <= 0) {

View File

@@ -1,10 +1,11 @@
import { describe, expect, it } from 'vitest'
import { NextRequest } from 'next/server'
import { GET } from './route'
describe('GET /api/health', () => {
it('returns OK status and permission level count', async () => {
const response = await GET(new Request('http://example.com/api/health'))
const response = await GET(new NextRequest('http://example.com/api/health'))
const payload = await response.json()
expect(payload.status).toBe('ok')

View File

@@ -3,10 +3,10 @@ import { NextResponse } from 'next/server'
import { readJson } from '@/lib/api/read-json'
import { setPackageData } from '@/lib/db/packages/set-package-data'
import type { JsonValue } from '@/types/utility-types'
import type { PackageSeedData } from '@/lib/package-types'
type PackageDataPayload = {
data?: Record<string, JsonValue[]>
data?: PackageSeedData
}
interface RouteParams {

View File

@@ -1,3 +1,5 @@
export const dynamic = 'force-dynamic'
export { DELETE } from './handlers/delete-package-data'
export { GET } from './handlers/get-package-data'
export { PUT } from './handlers/put-package-data'

View File

@@ -127,7 +127,7 @@ async function handleRequest(
// Build response with metadata
const responseData = result.meta
? { data: result.data, ...result.meta }
? { data: result.data, ...(result.meta as Record<string, unknown>) }
: result.data
// Map operation to appropriate status code

View File

@@ -28,12 +28,18 @@ export default async function DynamicUIPage({ params }: PageProps) {
const path = '/' + slug.join('/')
// Prefer Lua package-based UI pages, fallback to database-backed pages
const pageData = (await loadPageFromLuaPackages(path)) ?? (await loadPageFromDb(path))
const rawPageData = (await loadPageFromLuaPackages(path)) ?? (await loadPageFromDb(path))
if (!pageData) {
if (!rawPageData) {
notFound()
}
// Transform PageConfig to UIPageData
const pageData = {
layout: rawPageData,
actions: {},
}
// Check authentication if required
// TODO: Add auth check based on pageData.requireAuth and pageData.requiredRole

View File

@@ -28,7 +28,7 @@ const knownIcons = [
describe('getComponentIcon', () => {
it.each(knownIcons)('returns an icon for %s', iconName => {
expect(getComponentIcon(iconName, { sx: { fontSize: 20 } })).not.toBeNull()
expect(getComponentIcon(iconName, { style: { fontSize: 20 } })).not.toBeNull()
})
it('returns null for unknown icons', () => {

View File

@@ -1,4 +1,4 @@
import { act, renderHook, waitFor } from '@testing-library/react'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useAuth } from '@/hooks/useAuth'
@@ -7,6 +7,21 @@ import { login as loginRequest } from '@/lib/auth/api/login'
import { logout as logoutRequest } from '@/lib/auth/api/logout'
import type { User } from '@/lib/level-types'
// Simple waitFor implementation
const waitFor = async (callback: () => boolean | void, timeout = 1000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
try {
const result = callback()
if (result === false) continue
return
} catch {
await new Promise(resolve => setTimeout(resolve, 50))
}
}
throw new Error('waitFor timed out')
}
vi.mock('@/lib/auth/api/fetch-session', () => ({
fetchSession: vi.fn(),
}))

View File

@@ -1,4 +1,4 @@
import { act, renderHook, waitFor } from '@testing-library/react'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useAuth } from '@/hooks/useAuth'
@@ -8,6 +8,21 @@ import { logout as logoutRequest } from '@/lib/auth/api/logout'
import { register as registerRequest } from '@/lib/auth/api/register'
import type { User } from '@/lib/level-types'
// Simple waitFor implementation
const waitFor = async (callback: () => boolean | void, timeout = 1000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
try {
const result = callback()
if (result === false) continue
return
} catch {
await new Promise(resolve => setTimeout(resolve, 50))
}
}
throw new Error('waitFor timed out')
}
vi.mock('@/lib/auth/api/fetch-session', () => ({
fetchSession: vi.fn(),
}))

View File

@@ -13,7 +13,14 @@ import { getRoleLevel } from './role-levels'
*/
export const mapUserToAuthUser = (user: User): AuthUser => {
return {
...user,
id: user.id,
email: user.email,
username: user.username,
role: user.role as AuthUser['role'],
level: getRoleLevel(user.role),
tenantId: user.tenantId || undefined,
profilePicture: user.profilePicture || undefined,
bio: user.bio || undefined,
isInstanceOwner: user.isInstanceOwner,
}
}

View File

@@ -0,0 +1,25 @@
/**
* Hook for level-based routing functionality
*/
export interface LevelRouting {
canAccessLevel: (level: number) => boolean
redirectToLevel: (level: number) => void
}
/**
* Hook for managing level-based routing
* TODO: Implement full level routing logic
*/
export function useLevelRouting(): LevelRouting {
return {
canAccessLevel: (level: number) => {
// TODO: Implement level access check
return level >= 0
},
redirectToLevel: (level: number) => {
// TODO: Implement redirect logic (suppress console warning for now)
void level
},
}
}

View File

@@ -0,0 +1,22 @@
/**
* Hook for resolved user state
*/
export interface ResolvedUserState {
userId?: string
username?: string
level?: number
isLoading: boolean
error?: string
}
/**
* Hook for managing resolved user state
* TODO: Implement full user resolution logic
*/
export function useResolvedUser(): ResolvedUserState {
// TODO: Implement user resolution from session/auth
return {
isLoading: false,
}
}

View File

@@ -2,6 +2,7 @@ import { useCallback } from 'react'
// import { toast } from 'sonner'
import { dbal } from '@/lib/dbal/core/client'
import type { JsonValue } from '@/types/utility-types'
import { useDBAL } from './use-dbal'
@@ -17,7 +18,7 @@ export function useKVStore(tenantId: string = 'default', userId: string = 'syste
throw new Error('DBAL not ready')
}
try {
await dbal.kvSet(key, value, ttl, tenantId, userId)
await dbal.kvSet(key, value as JsonValue, ttl, tenantId, userId)
} catch (err) {
const _errorInfo = dbal.handleError(err)
// toast.error(`KV Set Error: ${errorInfo.message}`)
@@ -65,7 +66,7 @@ export function useKVStore(tenantId: string = 'default', userId: string = 'syste
throw new Error('DBAL not ready')
}
try {
await dbal.kvListAdd(key, items, tenantId, userId)
await dbal.kvListAdd(key, items as JsonValue[], tenantId, userId)
} catch (err) {
const _errorInfo = dbal.handleError(err)
// toast.error(`KV List Add Error: ${errorInfo.message}`)
@@ -81,7 +82,7 @@ export function useKVStore(tenantId: string = 'default', userId: string = 'syste
throw new Error('DBAL not ready')
}
try {
return await dbal.kvListGet(key, tenantId, userId, start, end)
return (await dbal.kvListGet(key, tenantId, userId, start, end)) as T[]
} catch (err) {
const _errorInfo = dbal.handleError(err)
// toast.error(`KV List Get Error: ${errorInfo.message}`)

View File

@@ -17,6 +17,7 @@ export async function compile(source: string, _options?: CompileOptions): Promis
return { code: source }
}
export async function loadAndInjectStyles(_packageId: string): Promise<void> {
export async function loadAndInjectStyles(_packageId: string): Promise<string> {
// TODO: Implement style loading and injection
return ''
}

View File

@@ -0,0 +1,32 @@
/**
* Component catalog for registering and retrieving components
*/
export interface ComponentCatalogEntry {
name: string
component: React.ComponentType
description?: string
}
/**
* Component catalog registry
* TODO: Implement full component catalog functionality
*/
export class ComponentCatalog {
private components = new Map<string, ComponentCatalogEntry>()
register(name: string, entry: ComponentCatalogEntry): void {
this.components.set(name, entry)
}
get(name: string): ComponentCatalogEntry | undefined {
return this.components.get(name)
}
getAll(): ComponentCatalogEntry[] {
return Array.from(this.components.values())
}
}
// Singleton instance
export const componentCatalog = new ComponentCatalog()

View File

@@ -0,0 +1,33 @@
/**
* Component registry for managing component metadata
*/
export interface ComponentMetadata {
id: string
name: string
type: string
props?: Record<string, unknown>
}
/**
* Component registry
* TODO: Implement full component registry functionality
*/
export class ComponentRegistry {
private registry = new Map<string, ComponentMetadata>()
register(id: string, metadata: ComponentMetadata): void {
this.registry.set(id, metadata)
}
get(id: string): ComponentMetadata | undefined {
return this.registry.get(id)
}
getAll(): ComponentMetadata[] {
return Array.from(this.registry.values())
}
}
// Singleton instance
export const componentRegistry = new ComponentRegistry()

View File

@@ -7,5 +7,5 @@ import type { User } from '../../types/level-types'
export async function dbalAddUser(user: User): Promise<void> {
const adapter = getAdapter()
await adapter.create('User', user)
await adapter.create('User', user as unknown as Record<string, unknown>)
}

View File

@@ -19,11 +19,11 @@ describe('addComment', () => {
const cases: Array<{ name: string; comment: Comment }> = [
{
name: 'basic comment',
comment: { id: 'c1', userId: 'u1', content: 'Hello', createdAt: 1000 },
comment: { id: 'c1', userId: 'u1', entityType: 'post', entityId: 'p1', content: 'Hello', createdAt: 1000 },
},
{
name: 'reply comment',
comment: { id: 'c2', userId: 'u1', content: 'Reply', createdAt: 2000, parentId: 'c1' },
comment: { id: 'c2', userId: 'u1', entityType: 'post', entityId: 'p1', content: 'Reply', createdAt: 2000, parentId: 'c1' },
},
]

View File

@@ -4,6 +4,8 @@ import type { Comment } from '@/lib/types/level-types'
type DBALCommentRecord = {
id: string
userId: string
entityType: string
entityId: string
content: string
createdAt: number | string | Date
updatedAt?: number | string | Date | null
@@ -19,6 +21,8 @@ export async function getComments(): Promise<Comment[]> {
return result.data.map(c => ({
id: c.id,
userId: c.userId,
entityType: c.entityType,
entityId: c.entityId,
content: c.content,
createdAt: Number(c.createdAt),
updatedAt: c.updatedAt ? Number(c.updatedAt) : undefined,

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../../../../core/dbal-client'
import type { ComponentConfig } from '../types'
import type { ComponentConfig } from '../../../types'
export async function addComponentConfig(config: ComponentConfig): Promise<void> {
const adapter = getAdapter()

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../../../../core/dbal-client'
import type { ComponentConfig } from '../types'
import type { ComponentConfig } from '../../../types'
export async function updateComponentConfig(
configId: string,

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../../../core/dbal-client'
import type { ComponentNode } from '../types'
import type { ComponentNode } from '../../types'
export async function addComponentNode(node: ComponentNode): Promise<void> {
const adapter = getAdapter()

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../../../core/dbal-client'
import type { ComponentNode } from '../types'
import type { ComponentNode } from '../../types'
export async function updateComponentNode(
nodeId: string,

View File

@@ -12,7 +12,7 @@ export interface ComponentConfig {
export interface ComponentNode {
id: string
name: string
name?: string
type: string
parentId?: string
childIds?: string[]

View File

@@ -21,11 +21,12 @@ export interface DropdownConfig {
*/
export interface ComponentNode {
id: string
name?: string
type: string
parentId?: string
childIds: string[]
order: number
pageId: string
childIds?: string[]
order?: number
pageId?: string
}
/**
@@ -36,7 +37,7 @@ export interface ComponentConfig {
componentId: string
props: Record<string, unknown>
styles: Record<string, unknown>
events: Record<string, string>
events?: Record<string, string>
conditionalRendering?: {
condition: string
luaScriptId?: string

View File

@@ -1,5 +1,5 @@
import { getAdapter } from '../../core/dbal-client'
import { mapSessionRecord } from '../map-session-record'
import { mapSessionRecord, type SessionRecord } from '../map-session-record'
import type { Session, UpdateSessionInput } from '../types'
export async function updateSession(
@@ -12,5 +12,5 @@ export async function updateSession(
...(input.expiresAt !== undefined ? { expiresAt: BigInt(input.expiresAt) } : {}),
...(input.lastActivity !== undefined ? { lastActivity: BigInt(input.lastActivity) } : {}),
})
return mapSessionRecord(record)
return mapSessionRecord(record as SessionRecord)
}

View File

@@ -1,10 +1,10 @@
import { getAdapter } from '../../core/dbal-client'
import { mapSessionRecord } from '../map-session-record'
import { mapSessionRecord, type SessionRecord } from '../map-session-record'
import type { Session } from '../types'
export async function getSessionById(sessionId: string): Promise<Session | null> {
const adapter = getAdapter()
const record = await adapter.read('Session', sessionId)
if (!record) return null
return mapSessionRecord(record)
return mapSessionRecord(record as SessionRecord)
}

View File

@@ -1,6 +1,6 @@
import { getAdapter } from '../../core/dbal-client'
import { deleteSession } from '../crud/delete/delete-session'
import { mapSessionRecord } from '../map-session-record'
import { mapSessionRecord, type SessionRecord } from '../map-session-record'
import type { Session } from '../types'
export async function getSessionByToken(token: string): Promise<Session | null> {
@@ -8,7 +8,7 @@ export async function getSessionByToken(token: string): Promise<Session | null>
const result = await adapter.list('Session', { filter: { token } })
if (!result.data.length) return null
const session = mapSessionRecord(result.data[0])
const session = mapSessionRecord(result.data[0] as SessionRecord)
if (session.expiresAt <= Date.now()) {
await deleteSession(session.id)
return null

View File

@@ -1,12 +1,12 @@
import type { Session } from './types'
type SessionRecord = {
export type SessionRecord = {
id: string
userId: string
token: string
expiresAt: number | string | Date
createdAt: number | string | Date
lastActivity: number | string | Date
expiresAt: number | string | Date | bigint
createdAt: number | string | Date | bigint
lastActivity: number | string | Date | bigint
}
export function mapSessionRecord(record: SessionRecord): Session {

View File

@@ -21,6 +21,20 @@ class PrismaAdapter implements DBALAdapter {
return await model.findUnique({ where: { id } })
}
async read(entity: string, id: string | number): Promise<unknown> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const model = (prisma as any)[entity.toLowerCase()]
if (!model) throw new Error(`Unknown entity: ${entity}`)
return await model.findUnique({ where: { id } })
}
async findFirst(entity: string, options: { where: Record<string, unknown> }): Promise<unknown> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const model = (prisma as any)[entity.toLowerCase()]
if (!model) throw new Error(`Unknown entity: ${entity}`)
return await model.findFirst({ where: options.where })
}
async list(entity: string, options?: ListOptions): Promise<ListResult> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const model = (prisma as any)[entity.toLowerCase()]
@@ -55,26 +69,42 @@ class PrismaAdapter implements DBALAdapter {
async upsert(
entity: string,
uniqueField: string,
uniqueValue: unknown,
createData: Record<string, unknown>,
updateData: Record<string, unknown>
uniqueFieldOrOptions: string | { where: Record<string, unknown>; update: Record<string, unknown>; create: Record<string, unknown> },
uniqueValue?: unknown,
createData?: Record<string, unknown>,
updateData?: Record<string, unknown>
): Promise<unknown> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const model = (prisma as any)[entity.toLowerCase()]
if (!model) throw new Error(`Unknown entity: ${entity}`)
// Handle options object form
if (typeof uniqueFieldOrOptions === 'object') {
return await model.upsert({
where: uniqueFieldOrOptions.where,
create: uniqueFieldOrOptions.create,
update: uniqueFieldOrOptions.update,
})
}
// Handle 5-parameter form
return await model.upsert({
where: { [uniqueField]: uniqueValue },
create: createData,
update: updateData,
where: { [uniqueFieldOrOptions]: uniqueValue },
create: createData!,
update: updateData!,
})
}
async delete(entity: string, id: string | number): Promise<void> {
async delete(entity: string, id: string | number): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const model = (prisma as any)[entity.toLowerCase()]
if (!model) throw new Error(`Unknown entity: ${entity}`)
await model.delete({ where: { id } })
try {
await model.delete({ where: { id } })
return true
} catch {
return false
}
}
async createMany(entity: string, data: Record<string, unknown>[]): Promise<unknown[]> {

View File

@@ -16,26 +16,31 @@ export interface ListResult<T = unknown> {
hasMore: boolean
}
export type UpsertOptions = { where: Record<string, unknown>; update: Record<string, unknown>; create: Record<string, unknown> }
export interface DBALAdapter {
// Create operations
create(entity: string, data: Record<string, unknown>): Promise<unknown>
// Read operations
get(entity: string, id: string | number): Promise<unknown>
read(entity: string, id: string | number): Promise<unknown>
findFirst(entity: string, options: { where: Record<string, unknown> }): Promise<unknown>
list(entity: string, options?: ListOptions): Promise<ListResult>
// Update operations
update(entity: string, id: string | number, data: Record<string, unknown>): Promise<unknown>
// Upsert has two signatures: 5-param form or options object form
upsert(
entity: string,
uniqueField: string,
uniqueValue: unknown,
createData: Record<string, unknown>,
updateData: Record<string, unknown>
uniqueFieldOrOptions: string | UpsertOptions,
uniqueValue?: unknown,
createData?: Record<string, unknown>,
updateData?: Record<string, unknown>
): Promise<unknown>
// Delete operations
delete(entity: string, id: string | number): Promise<void>
delete(entity: string, id: string | number): Promise<boolean>
// Batch operations
createMany(entity: string, data: Record<string, unknown>[]): Promise<unknown[]>

View File

@@ -1,6 +1,7 @@
import { DBALClient as _DBALClient, type DBALConfig as _DBALConfig } from '@/dbal'
import { InMemoryKVStore } from '@/dbal/core/kv'
import { MemoryStorage } from '@/dbal/blob/providers/memory-storage'
import { setInitialized } from './is-initialized'
interface DBALIntegrationState {
initialized?: boolean
@@ -10,24 +11,26 @@ interface DBALIntegrationState {
client?: _DBALClient
}
const state: DBALIntegrationState = {}
/**
* Initialize the DBAL client with configuration
*/
export async function initialize(this: DBALIntegrationState, config?: Partial<_DBALConfig>): Promise<void> {
if (this.initialized) {
export async function initialize(config?: Partial<_DBALConfig>): Promise<void> {
if (state.initialized) {
console.warn('DBAL already initialized')
return
}
try {
// Initialize tenant manager (stub for now)
this.tenantManager = { tenants: new Map() }
state.tenantManager = { tenants: new Map() }
// Initialize KV store
this.kvStore = new InMemoryKVStore()
state.kvStore = new InMemoryKVStore()
// Initialize blob storage
this.blobStorage = new MemoryStorage()
state.blobStorage = new MemoryStorage()
// Initialize DBAL client
const dbalConfig: _DBALConfig = {
@@ -36,9 +39,10 @@ export async function initialize(this: DBALIntegrationState, config?: Partial<_D
...config,
} as _DBALConfig
this.client = new _DBALClient(dbalConfig)
state.client = new _DBALClient(dbalConfig)
this.initialized = true
state.initialized = true
setInitialized(true)
} catch (error) {
console.error('Failed to initialize DBAL:', error)
throw error

View File

@@ -1,5 +1,10 @@
import type { DBALClient as _DBALClient, DBALConfig as _DBALConfig } from '@/dbal'
let initialized = false
export function isInitialized(): boolean {
return this.initialized
// TODO: Implement proper initialization state tracking
return initialized
}
export function setInitialized(value: boolean): void {
initialized = value
}

View File

@@ -43,6 +43,18 @@ import { getBlobStorage as getBlobStorageImpl } from './functions/get-blob-stora
import { getKVStore as getKVStoreImpl } from './functions/get-k-v-store'
import { getTenantContext as getTenantContextImpl } from './functions/get-tenant-context'
import { getTenantManager as getTenantManagerImpl } from './functions/get-tenant-manager'
import { handleError as handleErrorImpl } from './functions/handle-error'
import { isInitialized as isInitializedImpl } from './functions/is-initialized'
import { kvSet as kvSetImpl } from './functions/kv-set'
import { kvGet as kvGetImpl } from './functions/kv-get'
import { kvDelete as kvDeleteImpl } from './functions/kv-delete'
import { kvListAdd as kvListAddImpl } from './functions/kv-list-add'
import { kvListGet as kvListGetImpl } from './functions/kv-list-get'
import { blobUpload as blobUploadImpl } from './functions/blob-upload'
import { blobDownload as blobDownloadImpl } from './functions/blob-download'
import { blobDelete as blobDeleteImpl } from './functions/blob-delete'
import { blobList as blobListImpl } from './functions/blob-list'
import { blobGetMetadata as blobGetMetadataImpl } from './functions/blob-get-metadata'
// Create a namespace object for backward compatibility
export const dbal = {
@@ -55,6 +67,18 @@ export const dbal = {
getKVStore: getKVStoreImpl,
getTenantContext: getTenantContextImpl,
getTenantManager: getTenantManagerImpl,
handleError: handleErrorImpl,
isInitialized: isInitializedImpl,
kvSet: kvSetImpl,
kvGet: kvGetImpl,
kvDelete: kvDeleteImpl,
kvListAdd: kvListAddImpl,
kvListGet: kvListGetImpl,
blobUpload: blobUploadImpl,
blobDownload: blobDownloadImpl,
blobDelete: blobDeleteImpl,
blobList: blobListImpl,
blobGetMetadata: blobGetMetadataImpl,
}
// Type alias for backward compatibility

View File

@@ -2,13 +2,15 @@
* Generate component tree from Lua (stub)
*/
import type { ReactNode } from 'react'
export interface ComponentTree {
type: string
props?: Record<string, unknown>
children?: ComponentTree[]
}
export function generateComponentTree(_luaScript: string): ComponentTree {
export function generateComponentTree(_luaScript: unknown): ReactNode {
// TODO: Implement Lua component tree generation
return { type: 'div' }
return null
}

View File

@@ -2,6 +2,12 @@
* Lua UI types (stub)
*/
export interface LuaUIComponent {
type: string
props?: Record<string, unknown>
children?: LuaUIComponent[]
}
export interface LuaUIPackage {
id: string
name: string

View File

@@ -4,7 +4,14 @@
import type { PageConfig } from '../types/level-types'
export async function loadPageFromDb(_b_path: string, _tenantId?: string): Promise<PageConfig | null> {
export type LuaActionHandler = (action: string, data: Record<string, unknown>) => void | Promise<void>
export interface UIPageData {
layout: unknown
actions?: Record<string, LuaActionHandler>
}
export async function loadPageFromDb(_path: string, _tenantId?: string): Promise<PageConfig | null> {
// TODO: Implement page loading from database
return null
}

View File

@@ -20,7 +20,7 @@
"@storybook/addon-essentials": "^8.6.15",
"@storybook/addon-interactions": "^8.6.15",
"@storybook/react": "^10.1.11",
"@storybook/react-vite": "^8.6.15",
"@storybook/react-vite": "^10.1.11",
"@storybook/test": "^8.6.15",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",