feat(validation): enhance entity validation with new fields and improved checks

- Updated page validation to include new properties such as path, componentTree, requiresAuth, and more.
- Enhanced session validation to check for bigint timestamps and added checks for ipAddress and userAgent.
- Improved user validation by adding checks for profilePicture, bio, createdAt, and tenantId.
- Refactored workflow validation to ensure nodes and edges are valid JSON strings and added checks for createdAt and updatedAt.
- Introduced isValidJsonString utility for validating JSON strings.
- Added seedUsers function to populate default users in the database.
- Removed unused generateComponentTree stub and replaced it with a full implementation for rendering Lua UI components.
- Updated useLevelRouting and useResolvedUser hooks to support new permission levels and user state management.
- Enhanced useRestApi hook to support request cancellation with AbortSignal.
This commit is contained in:
2026-01-07 12:50:58 +00:00
parent 4caf9e2ae9
commit 6c8e7002cd
46 changed files with 1233 additions and 554 deletions

View File

@@ -1,23 +1,28 @@
/**
* @file component-operations.ts
* @description ComponentHierarchy entity CRUD operations for DBAL client
* @description ComponentNode entity CRUD operations for DBAL client
*/
import { randomUUID } from 'crypto'
import type { DBALAdapter } from '../../../../adapters/adapter'
import type { ComponentHierarchy, ListResult, PageView } from '../../../foundation/types'
import type {
ComponentNode,
CreateComponentNodeInput,
ListResult,
PageConfig,
UpdateComponentNodeInput,
} from '../../../foundation/types'
import { DBALError } from '../../../foundation/errors'
import { validateComponentHierarchyCreate, validateComponentHierarchyUpdate, validateId } from '../../../foundation/validation'
export interface ComponentOperations {
create: (data: ComponentCreatePayload) => Promise<ComponentHierarchy>
read: (id: string) => Promise<ComponentHierarchy | null>
update: (id: string, data: Partial<ComponentHierarchy>) => Promise<ComponentHierarchy>
export interface ComponentNodeOperations {
create: (data: CreateComponentNodeInput) => Promise<ComponentNode>
read: (id: string) => Promise<ComponentNode | null>
update: (id: string, data: UpdateComponentNodeInput) => Promise<ComponentNode>
delete: (id: string) => Promise<boolean>
getTree: (pageId: string) => Promise<ComponentHierarchy[]>
getTree: (pageId: string) => Promise<ComponentNode[]>
}
type ComponentCreatePayload = Omit<ComponentHierarchy, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'> & { tenantId?: string }
const assertValidId = (id: string) => {
const errors = validateId(id)
if (errors.length > 0) {
@@ -25,21 +30,21 @@ const assertValidId = (id: string) => {
}
}
const assertValidCreate = (data: ComponentCreatePayload) => {
const assertValidCreate = (data: CreateComponentNodeInput) => {
const errors = validateComponentHierarchyCreate(data)
if (errors.length > 0) {
throw DBALError.validationError('Invalid component data', errors.map(error => ({ field: 'component', error })))
}
}
const assertValidUpdate = (data: Partial<ComponentHierarchy>) => {
const assertValidUpdate = (data: UpdateComponentNodeInput) => {
const errors = validateComponentHierarchyUpdate(data)
if (errors.length > 0) {
throw DBALError.validationError('Invalid component update data', errors.map(error => ({ field: 'component', error })))
}
}
const resolveTenantId = (configuredTenantId?: string, data?: Partial<ComponentHierarchy>): string | null => {
const resolveTenantId = (configuredTenantId?: string, data?: { tenantId?: string | null }): string | null => {
if (configuredTenantId && configuredTenantId.length > 0) return configuredTenantId
const candidate = data?.tenantId
if (typeof candidate === 'string' && candidate.length > 0) return candidate
@@ -47,22 +52,31 @@ const resolveTenantId = (configuredTenantId?: string, data?: Partial<ComponentHi
}
const assertPageTenant = async (adapter: DBALAdapter, tenantId: string, pageId: string) => {
const page = await adapter.findFirst('PageView', { id: pageId, tenantId }) as PageView | null
const page = await adapter.findFirst('PageConfig', { id: pageId, tenantId }) as PageConfig | null
if (!page) {
throw DBALError.notFound(`Page not found: ${pageId}`)
}
}
export const createComponentOperations = (adapter: DBALAdapter, tenantId?: string): ComponentOperations => ({
const withComponentDefaults = (data: CreateComponentNodeInput): ComponentNode => ({
id: data.id ?? randomUUID(),
type: data.type,
parentId: data.parentId ?? null,
childIds: data.childIds,
order: data.order,
pageId: data.pageId,
})
export const createComponentNodeOperations = (adapter: DBALAdapter, tenantId?: string): ComponentNodeOperations => ({
create: async data => {
const resolvedTenantId = resolveTenantId(tenantId, data)
const resolvedTenantId = resolveTenantId(tenantId)
if (!resolvedTenantId) {
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
assertValidCreate(data)
await assertPageTenant(adapter, resolvedTenantId, data.pageId)
const payload = { ...data, tenantId: resolvedTenantId }
return adapter.create('ComponentHierarchy', payload) as Promise<ComponentHierarchy>
const payload = withComponentDefaults(data)
return adapter.create('ComponentNode', payload) as Promise<ComponentNode>
},
read: async id => {
const resolvedTenantId = resolveTenantId(tenantId)
@@ -70,30 +84,29 @@ export const createComponentOperations = (adapter: DBALAdapter, tenantId?: strin
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
assertValidId(id)
const result = await adapter.findFirst('ComponentHierarchy', { id, tenantId: resolvedTenantId }) as ComponentHierarchy | null
const result = await adapter.findFirst('ComponentNode', { id }) as ComponentNode | null
if (!result) {
throw DBALError.notFound(`Component not found: ${id}`)
}
await assertPageTenant(adapter, resolvedTenantId, result.pageId)
return result
},
update: async (id, data) => {
if (data.tenantId !== undefined) {
throw DBALError.validationError('Tenant ID cannot be updated', [{ field: 'tenantId', error: 'tenantId is immutable' }])
}
const resolvedTenantId = resolveTenantId(tenantId)
if (!resolvedTenantId) {
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
assertValidId(id)
assertValidUpdate(data)
if (data.pageId) {
await assertPageTenant(adapter, resolvedTenantId, data.pageId)
}
const existing = await adapter.findFirst('ComponentHierarchy', { id, tenantId: resolvedTenantId }) as ComponentHierarchy | null
const existing = await adapter.findFirst('ComponentNode', { id }) as ComponentNode | null
if (!existing) {
throw DBALError.notFound(`Component not found: ${id}`)
}
return adapter.update('ComponentHierarchy', id, data) as Promise<ComponentHierarchy>
await assertPageTenant(adapter, resolvedTenantId, existing.pageId)
if (data.pageId) {
await assertPageTenant(adapter, resolvedTenantId, data.pageId)
}
return adapter.update('ComponentNode', id, data) as Promise<ComponentNode>
},
delete: async id => {
const resolvedTenantId = resolveTenantId(tenantId)
@@ -101,11 +114,12 @@ export const createComponentOperations = (adapter: DBALAdapter, tenantId?: strin
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
assertValidId(id)
const existing = await adapter.findFirst('ComponentHierarchy', { id, tenantId: resolvedTenantId }) as ComponentHierarchy | null
const existing = await adapter.findFirst('ComponentNode', { id }) as ComponentNode | null
if (!existing) {
throw DBALError.notFound(`Component not found: ${id}`)
}
const result = await adapter.delete('ComponentHierarchy', id)
await assertPageTenant(adapter, resolvedTenantId, existing.pageId)
const result = await adapter.delete('ComponentNode', id)
if (!result) {
throw DBALError.notFound(`Component not found: ${id}`)
}
@@ -118,10 +132,12 @@ export const createComponentOperations = (adapter: DBALAdapter, tenantId?: strin
}
assertValidId(pageId)
await assertPageTenant(adapter, resolvedTenantId, pageId)
const result = await adapter.list('ComponentHierarchy', {
filter: { pageId, tenantId: resolvedTenantId },
const result = await adapter.list('ComponentNode', {
filter: { pageId },
sort: { order: 'asc' },
}) as ListResult<ComponentHierarchy>
}) as ListResult<ComponentNode>
return result.data
},
})
export const createComponentOperations = createComponentNodeOperations

View File

@@ -1,11 +1,11 @@
import type { DBALAdapter } from '../../../../../adapters/adapter'
import type { Package } from '../../../../foundation/types'
import type { InstalledPackage } from '../../../../foundation/types'
import { DBALError } from '../../../../foundation/errors'
import { validatePackageCreate, validatePackageUpdate } from '../../../../foundation/validation'
export const createManyPackages = async (
export const createManyInstalledPackages = async (
adapter: DBALAdapter,
data: Array<Omit<Package, 'id' | 'createdAt' | 'updatedAt'>>,
data: InstalledPackage[],
): Promise<number> => {
if (!data || data.length === 0) {
return 0
@@ -15,23 +15,23 @@ export const createManyPackages = async (
validatePackageCreate(item).map(error => ({ field: `packages[${index}]`, error })),
)
if (validationErrors.length > 0) {
throw DBALError.validationError('Invalid package batch', validationErrors)
throw DBALError.validationError('Invalid installed package batch', validationErrors)
}
try {
return adapter.createMany('Package', data as Record<string, unknown>[])
return adapter.createMany('InstalledPackage', data as Record<string, unknown>[])
} catch (error) {
if (error instanceof DBALError && error.code === 409) {
throw DBALError.conflict('Package name+version already exists')
throw DBALError.conflict('Installed package already exists')
}
throw error
}
}
export const updateManyPackages = async (
export const updateManyInstalledPackages = async (
adapter: DBALAdapter,
filter: Record<string, unknown>,
data: Partial<Package>,
data: Partial<InstalledPackage>,
): Promise<number> => {
if (!filter || Object.keys(filter).length === 0) {
throw DBALError.validationError('Bulk update requires a filter', [
@@ -47,25 +47,31 @@ export const updateManyPackages = async (
const validationErrors = validatePackageUpdate(data)
if (validationErrors.length > 0) {
throw DBALError.validationError('Invalid package update data', validationErrors.map(error => ({ field: 'package', error })))
throw DBALError.validationError(
'Invalid installed package update data',
validationErrors.map(error => ({ field: 'package', error })),
)
}
try {
return adapter.updateMany('Package', filter, data as Record<string, unknown>)
return adapter.updateMany('InstalledPackage', filter, data as Record<string, unknown>)
} catch (error) {
if (error instanceof DBALError && error.code === 409) {
throw DBALError.conflict('Package name+version already exists')
throw DBALError.conflict('Installed package already exists')
}
throw error
}
}
export const deleteManyPackages = async (adapter: DBALAdapter, filter: Record<string, unknown>): Promise<number> => {
export const deleteManyInstalledPackages = async (
adapter: DBALAdapter,
filter: Record<string, unknown>,
): Promise<number> => {
if (!filter || Object.keys(filter).length === 0) {
throw DBALError.validationError('Bulk delete requires a filter', [
{ field: 'filter', error: 'Filter is required' },
])
}
return adapter.deleteMany('Package', filter)
return adapter.deleteMany('InstalledPackage', filter)
}

View File

@@ -1,61 +1,70 @@
import type { DBALAdapter } from '../../../../../adapters/adapter'
import type { Package } from '../../../../foundation/types'
import type { InstalledPackage } from '../../../../foundation/types'
import { DBALError } from '../../../../foundation/errors'
import { validatePackageCreate, validatePackageUpdate, validateId } from '../../../../foundation/validation'
export const createPackage = async (
export const createInstalledPackage = async (
adapter: DBALAdapter,
data: Omit<Package, 'id' | 'createdAt' | 'updatedAt'>,
): Promise<Package> => {
data: InstalledPackage,
): Promise<InstalledPackage> => {
const validationErrors = validatePackageCreate(data)
if (validationErrors.length > 0) {
throw DBALError.validationError('Invalid package data', validationErrors.map(error => ({ field: 'package', error })))
throw DBALError.validationError(
'Invalid installed package data',
validationErrors.map(error => ({ field: 'package', error })),
)
}
try {
return adapter.create('Package', data) as Promise<Package>
return adapter.create('InstalledPackage', data) as Promise<InstalledPackage>
} catch (error) {
if (error instanceof DBALError && error.code === 409) {
throw DBALError.conflict(`Package ${data.name}@${data.version} already exists`)
throw DBALError.conflict(`Installed package ${data.packageId} already exists`)
}
throw error
}
}
export const updatePackage = async (
export const updateInstalledPackage = async (
adapter: DBALAdapter,
id: string,
data: Partial<Package>,
): Promise<Package> => {
const idErrors = validateId(id)
packageId: string,
data: Partial<InstalledPackage>,
): Promise<InstalledPackage> => {
const idErrors = validateId(packageId)
if (idErrors.length > 0) {
throw DBALError.validationError('Invalid package ID', idErrors.map(error => ({ field: 'id', error })))
throw DBALError.validationError('Invalid package ID', idErrors.map(error => ({ field: 'packageId', error })))
}
const validationErrors = validatePackageUpdate(data)
if (validationErrors.length > 0) {
throw DBALError.validationError('Invalid package update data', validationErrors.map(error => ({ field: 'package', error })))
throw DBALError.validationError(
'Invalid installed package update data',
validationErrors.map(error => ({ field: 'package', error })),
)
}
try {
return adapter.update('Package', id, data) as Promise<Package>
return adapter.update('InstalledPackage', packageId, data) as Promise<InstalledPackage>
} catch (error) {
if (error instanceof DBALError && error.code === 409) {
throw DBALError.conflict('Package name+version already exists')
throw DBALError.conflict('Installed package already exists')
}
throw error
}
}
export const deletePackage = async (adapter: DBALAdapter, id: string): Promise<boolean> => {
const validationErrors = validateId(id)
export const deleteInstalledPackage = async (
adapter: DBALAdapter,
packageId: string,
): Promise<boolean> => {
const validationErrors = validateId(packageId)
if (validationErrors.length > 0) {
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'id', error })))
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'packageId', error })))
}
const result = await adapter.delete('Package', id)
const result = await adapter.delete('InstalledPackage', packageId)
if (!result) {
throw DBALError.notFound(`Package not found: ${id}`)
throw DBALError.notFound(`Installed package not found: ${packageId}`)
}
return result
}

View File

@@ -1,10 +1,10 @@
import type { DBALAdapter } from '../../../../../adapters/adapter'
import type { Package } from '../../../../foundation/types'
import { createPackage } from './mutations'
import type { InstalledPackage } from '../../../../foundation/types'
import { createInstalledPackage } from './mutations'
export const publishPackage = (
adapter: DBALAdapter,
data: Omit<Package, 'id' | 'createdAt' | 'updatedAt'>,
): Promise<Package> => {
return createPackage(adapter, data)
data: InstalledPackage,
): Promise<InstalledPackage> => {
return createInstalledPackage(adapter, data)
}

View File

@@ -1,21 +1,27 @@
import type { DBALAdapter } from '../../../../../adapters/adapter'
import type { Package, ListOptions, ListResult } from '../../../../foundation/types'
import type { InstalledPackage, ListOptions, ListResult } from '../../../../foundation/types'
import { DBALError } from '../../../../foundation/errors'
import { validateId } from '../../../../foundation/validation'
export const readPackage = async (adapter: DBALAdapter, id: string): Promise<Package | null> => {
const validationErrors = validateId(id)
export const readInstalledPackage = async (
adapter: DBALAdapter,
packageId: string,
): Promise<InstalledPackage | null> => {
const validationErrors = validateId(packageId)
if (validationErrors.length > 0) {
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'id', error })))
throw DBALError.validationError('Invalid package ID', validationErrors.map(error => ({ field: 'packageId', error })))
}
const result = await adapter.read('Package', id) as Package | null
const result = await adapter.read('InstalledPackage', packageId) as InstalledPackage | null
if (!result) {
throw DBALError.notFound(`Package not found: ${id}`)
throw DBALError.notFound(`Installed package not found: ${packageId}`)
}
return result
}
export const listPackages = (adapter: DBALAdapter, options?: ListOptions): Promise<ListResult<Package>> => {
return adapter.list('Package', options) as Promise<ListResult<Package>>
export const listInstalledPackages = (
adapter: DBALAdapter,
options?: ListOptions,
): Promise<ListResult<InstalledPackage>> => {
return adapter.list('InstalledPackage', options) as Promise<ListResult<InstalledPackage>>
}

View File

@@ -1,6 +1,6 @@
import type { DBALAdapter } from '../../../../../adapters/adapter'
import { deletePackage } from './mutations'
import { deleteInstalledPackage } from './mutations'
export const unpublishPackage = (adapter: DBALAdapter, id: string): Promise<boolean> => {
return deletePackage(adapter, id)
return deleteInstalledPackage(adapter, id)
}

View File

@@ -1,6 +1,6 @@
import type { Package } from '../../../../foundation/types'
import type { InstalledPackage } from '../../../../foundation/types'
import { validatePackageCreate } from '../../../../foundation/validation'
export const validatePackage = (data: Partial<Package>): string[] => {
export const validatePackage = (data: Partial<InstalledPackage>): string[] => {
return validatePackageCreate(data)
}

View File

@@ -1,24 +1,23 @@
/**
* @file page-operations.ts
* @description PageView entity CRUD operations for DBAL client
* @description PageConfig entity CRUD operations for DBAL client
*/
import { randomUUID } from 'crypto'
import type { DBALAdapter } from '../../../../adapters/adapter'
import type { ListOptions, ListResult, PageView } from '../../../foundation/types'
import type { CreatePageInput, ListOptions, ListResult, PageConfig, UpdatePageInput } from '../../../foundation/types'
import { DBALError } from '../../../foundation/errors'
import { isValidSlug, validateId, validatePageCreate, validatePageUpdate } from '../../../foundation/validation'
import { validateId, validatePageCreate, validatePageUpdate } from '../../../foundation/validation'
export interface PageOperations {
create: (data: PageCreatePayload) => Promise<PageView>
read: (id: string) => Promise<PageView | null>
readBySlug: (slug: string) => Promise<PageView | null>
update: (id: string, data: Partial<PageView>) => Promise<PageView>
export interface PageConfigOperations {
create: (data: CreatePageInput) => Promise<PageConfig>
read: (id: string) => Promise<PageConfig | null>
readByPath: (path: string) => Promise<PageConfig | null>
update: (id: string, data: UpdatePageInput) => Promise<PageConfig>
delete: (id: string) => Promise<boolean>
list: (options?: ListOptions) => Promise<ListResult<PageView>>
list: (options?: ListOptions) => Promise<ListResult<PageConfig>>
}
type PageCreatePayload = Omit<PageView, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'> & { tenantId?: string }
const assertValidId = (id: string) => {
const errors = validateId(id)
if (errors.length > 0) {
@@ -26,27 +25,27 @@ const assertValidId = (id: string) => {
}
}
const assertValidSlug = (slug: string) => {
if (!slug || !isValidSlug(slug)) {
throw DBALError.validationError('Invalid page slug', [{ field: 'slug', error: 'slug is invalid' }])
const assertValidPath = (path: string) => {
if (!path || typeof path !== 'string' || path.trim().length === 0) {
throw DBALError.validationError('Invalid page path', [{ field: 'path', error: 'path is invalid' }])
}
}
const assertValidCreate = (data: PageCreatePayload) => {
const assertValidCreate = (data: CreatePageInput) => {
const errors = validatePageCreate(data)
if (errors.length > 0) {
throw DBALError.validationError('Invalid page data', errors.map(error => ({ field: 'page', error })))
}
}
const assertValidUpdate = (data: Partial<PageView>) => {
const assertValidUpdate = (data: UpdatePageInput) => {
const errors = validatePageUpdate(data)
if (errors.length > 0) {
throw DBALError.validationError('Invalid page update data', errors.map(error => ({ field: 'page', error })))
}
}
const resolveTenantId = (configuredTenantId?: string, data?: Partial<PageView>): string | null => {
const resolveTenantId = (configuredTenantId?: string, data?: Partial<PageConfig>): string | null => {
if (configuredTenantId && configuredTenantId.length > 0) return configuredTenantId
const candidate = data?.tenantId
if (typeof candidate === 'string' && candidate.length > 0) return candidate
@@ -67,21 +66,44 @@ const resolveTenantFilter = (
return null
}
export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): PageOperations => ({
const withPageDefaults = (data: CreatePageInput): PageConfig => {
const now = BigInt(Date.now())
return {
id: data.id ?? randomUUID(),
tenantId: data.tenantId ?? null,
packageId: data.packageId ?? null,
path: data.path,
title: data.title,
description: data.description ?? null,
icon: data.icon ?? null,
component: data.component ?? null,
componentTree: data.componentTree,
level: data.level,
requiresAuth: data.requiresAuth,
requiredRole: data.requiredRole ?? null,
parentPath: data.parentPath ?? null,
sortOrder: data.sortOrder ?? 0,
isPublished: data.isPublished ?? true,
params: data.params ?? null,
meta: data.meta ?? null,
createdAt: data.createdAt ?? now,
updatedAt: data.updatedAt ?? now,
}
}
export const createPageConfigOperations = (adapter: DBALAdapter, tenantId?: string): PageConfigOperations => ({
create: async data => {
const resolvedTenantId = resolveTenantId(tenantId, data)
if (!resolvedTenantId) {
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
const isActive = data.isActive ?? true
const payload = { ...data, tenantId: resolvedTenantId, isActive }
const payload = withPageDefaults({ ...data, tenantId: resolvedTenantId })
assertValidCreate(payload)
try {
return adapter.create('PageView', payload) as Promise<PageView>
return adapter.create('PageConfig', payload) as Promise<PageConfig>
} catch (error) {
if (error instanceof DBALError && error.code === 409) {
const slug = typeof data.slug === 'string' ? data.slug : 'unknown'
throw DBALError.conflict(`Page with slug '${slug}' already exists`)
throw DBALError.conflict(`Page with path '${data.path}' already exists`)
}
throw error
}
@@ -92,21 +114,21 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
assertValidId(id)
const result = await adapter.findFirst('PageView', { id, tenantId: resolvedTenantId }) as PageView | null
const result = await adapter.findFirst('PageConfig', { id, tenantId: resolvedTenantId }) as PageConfig | null
if (!result) {
throw DBALError.notFound(`Page not found: ${id}`)
}
return result
},
readBySlug: async slug => {
readByPath: async path => {
const resolvedTenantId = resolveTenantId(tenantId)
if (!resolvedTenantId) {
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
assertValidSlug(slug)
const result = await adapter.findFirst('PageView', { slug, tenantId: resolvedTenantId }) as PageView | null
assertValidPath(path)
const result = await adapter.findFirst('PageConfig', { path, tenantId: resolvedTenantId }) as PageConfig | null
if (!result) {
throw DBALError.notFound(`Page not found with slug: ${slug}`)
throw DBALError.notFound(`Page not found with path: ${path}`)
}
return result
},
@@ -120,18 +142,15 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
}
assertValidId(id)
assertValidUpdate(data)
const existing = await adapter.findFirst('PageView', { id, tenantId: resolvedTenantId }) as PageView | null
const existing = await adapter.findFirst('PageConfig', { id, tenantId: resolvedTenantId }) as PageConfig | null
if (!existing) {
throw DBALError.notFound(`Page not found: ${id}`)
}
try {
return adapter.update('PageView', id, data) as Promise<PageView>
return adapter.update('PageConfig', id, data) as Promise<PageConfig>
} catch (error) {
if (error instanceof DBALError && error.code === 409) {
if (typeof data.slug === 'string') {
throw DBALError.conflict(`Page with slug '${data.slug}' already exists`)
}
throw DBALError.conflict('Page slug already exists')
throw DBALError.conflict('Page path already exists')
}
throw error
}
@@ -142,11 +161,11 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
assertValidId(id)
const existing = await adapter.findFirst('PageView', { id, tenantId: resolvedTenantId }) as PageView | null
const existing = await adapter.findFirst('PageConfig', { id, tenantId: resolvedTenantId }) as PageConfig | null
if (!existing) {
throw DBALError.notFound(`Page not found: ${id}`)
}
const result = await adapter.delete('PageView', id)
const result = await adapter.delete('PageConfig', id)
if (!result) {
throw DBALError.notFound(`Page not found: ${id}`)
}
@@ -157,6 +176,8 @@ export const createPageOperations = (adapter: DBALAdapter, tenantId?: string): P
if (!tenantFilter) {
throw DBALError.validationError('Tenant ID is required', [{ field: 'tenantId', error: 'tenantId is required' }])
}
return adapter.list('PageView', { ...options, filter: tenantFilter }) as Promise<ListResult<PageView>>
return adapter.list('PageConfig', { ...options, filter: tenantFilter }) as Promise<ListResult<PageConfig>>
},
})
export const createPageOperations = createPageConfigOperations

View File

@@ -1,30 +1,35 @@
export interface Credential {
id: string
username: string
passwordHash: string
firstLogin: boolean
createdAt: Date
updatedAt: Date
}
export interface Session {
id: string
userId: string
token: string
expiresAt: Date
createdAt: Date
lastActivity: Date
expiresAt: bigint
createdAt: bigint
lastActivity: bigint
ipAddress?: string | null
userAgent?: string | null
}
export interface CreateSessionInput {
id?: string
userId: string
token: string
expiresAt: Date
expiresAt: bigint
createdAt?: bigint
lastActivity?: bigint
ipAddress?: string | null
userAgent?: string | null
}
export interface UpdateSessionInput {
userId?: string
token?: string
expiresAt?: Date
lastActivity?: Date
expiresAt?: bigint
lastActivity?: bigint
ipAddress?: string | null
userAgent?: string | null
}

View File

@@ -1,71 +1,90 @@
export interface Workflow {
id: string
tenantId: string
name: string
description?: string
trigger: 'manual' | 'schedule' | 'event' | 'webhook'
triggerConfig: Record<string, unknown>
steps: Record<string, unknown>
isActive: boolean
createdBy: string
createdAt: Date
updatedAt: Date
nodes: string
edges: string
enabled: boolean
version: number
createdAt?: bigint | null
updatedAt?: bigint | null
createdBy?: string | null
tenantId?: string | null
}
export interface CreateWorkflowInput {
tenantId?: string
id?: string
name: string
description?: string
trigger: Workflow['trigger']
triggerConfig: Record<string, unknown>
steps: Record<string, unknown>
isActive?: boolean
createdBy: string
nodes: string
edges: string
enabled: boolean
version?: number
createdAt?: bigint | null
updatedAt?: bigint | null
createdBy?: string | null
tenantId?: string | null
}
export interface UpdateWorkflowInput {
tenantId?: string
name?: string
description?: string
trigger?: Workflow['trigger']
triggerConfig?: Record<string, unknown>
steps?: Record<string, unknown>
isActive?: boolean
createdBy?: string
nodes?: string
edges?: string
enabled?: boolean
version?: number
createdAt?: bigint | null
updatedAt?: bigint | null
createdBy?: string | null
tenantId?: string | null
}
export interface LuaScript {
id: string
tenantId: string
name: string
description?: string
code: string
parameters: string
returnType?: string | null
isSandboxed: boolean
allowedGlobals: string[]
allowedGlobals: string
timeoutMs: number
createdBy: string
createdAt: Date
updatedAt: Date
version: number
createdAt?: bigint | null
updatedAt?: bigint | null
createdBy?: string | null
tenantId?: string | null
}
export interface CreateLuaScriptInput {
tenantId?: string
id?: string
name: string
description?: string
code: string
parameters: string
returnType?: string | null
isSandboxed?: boolean
allowedGlobals: string[]
allowedGlobals: string
timeoutMs?: number
createdBy: string
version?: number
createdAt?: bigint | null
updatedAt?: bigint | null
createdBy?: string | null
tenantId?: string | null
}
export interface UpdateLuaScriptInput {
tenantId?: string
name?: string
description?: string
code?: string
parameters?: string
returnType?: string | null
isSandboxed?: boolean
allowedGlobals?: string[]
allowedGlobals?: string
timeoutMs?: number
createdBy?: string
version?: number
createdAt?: bigint | null
updatedAt?: bigint | null
createdBy?: string | null
tenantId?: string | null
}

View File

@@ -1,44 +1,116 @@
export interface PageView {
export interface PageConfig {
id: string
tenantId: string
slug: string
tenantId?: string | null
packageId?: string | null
path: string
title: string
description?: string
description?: string | null
icon?: string | null
component?: string | null
componentTree: string
level: number
layout: Record<string, unknown>
isActive: boolean
createdAt: Date
updatedAt: Date
requiresAuth: boolean
requiredRole?: string | null
parentPath?: string | null
sortOrder: number
isPublished: boolean
params?: string | null
meta?: string | null
createdAt?: bigint | null
updatedAt?: bigint | null
}
export interface CreatePageInput {
tenantId?: string
slug: string
id?: string
tenantId?: string | null
packageId?: string | null
path: string
title: string
description?: string
description?: string | null
icon?: string | null
component?: string | null
componentTree: string
level: number
layout: Record<string, unknown>
isActive?: boolean
requiresAuth: boolean
requiredRole?: string | null
parentPath?: string | null
sortOrder?: number
isPublished?: boolean
params?: string | null
meta?: string | null
createdAt?: bigint | null
updatedAt?: bigint | null
}
export interface UpdatePageInput {
tenantId?: string
slug?: string
tenantId?: string | null
packageId?: string | null
path?: string
title?: string
description?: string
description?: string | null
icon?: string | null
component?: string | null
componentTree?: string
level?: number
layout?: Record<string, unknown>
isActive?: boolean
requiresAuth?: boolean
requiredRole?: string | null
parentPath?: string | null
sortOrder?: number
isPublished?: boolean
params?: string | null
meta?: string | null
createdAt?: bigint | null
updatedAt?: bigint | null
}
export interface ComponentHierarchy {
export interface ComponentNode {
id: string
tenantId: string
pageId: string
parentId?: string
componentType: string
type: string
parentId?: string | null
childIds: string
order: number
props: Record<string, unknown>
createdAt: Date
updatedAt: Date
pageId: string
}
export interface CreateComponentNodeInput {
id?: string
type: string
parentId?: string | null
childIds: string
order: number
pageId: string
}
export interface UpdateComponentNodeInput {
type?: string
parentId?: string | null
childIds?: string
order?: number
pageId?: string
}
export interface ComponentConfig {
id: string
componentId: string
props: string
styles: string
events: string
conditionalRendering?: string | null
}
export interface CreateComponentConfigInput {
id?: string
componentId: string
props: string
styles: string
events: string
conditionalRendering?: string | null
}
export interface UpdateComponentConfigInput {
componentId?: string
props?: string
styles?: string
events?: string
conditionalRendering?: string | null
}

View File

@@ -11,7 +11,7 @@ export interface SoftDeletableEntity extends BaseEntity {
}
export interface TenantScopedEntity extends BaseEntity {
tenantId: string
tenantId?: string | null
}
export interface EntityMetadata {

View File

@@ -1,38 +1,39 @@
export interface Package {
id: string
tenantId: string
name: string
export interface InstalledPackage {
packageId: string
tenantId?: string | null
installedAt: bigint
version: string
description?: string
author: string
manifest: Record<string, unknown>
isInstalled: boolean
installedAt?: Date
installedBy?: string
createdAt: Date
updatedAt: Date
enabled: boolean
config?: string | null
}
export interface CreatePackageInput {
tenantId?: string
name: string
packageId: string
tenantId?: string | null
installedAt: bigint
version: string
description?: string
author: string
manifest: Record<string, unknown>
isInstalled?: boolean
installedAt?: Date
installedBy?: string
enabled: boolean
config?: string | null
}
export interface UpdatePackageInput {
tenantId?: string
name?: string
tenantId?: string | null
installedAt?: bigint
version?: string
description?: string
author?: string
manifest?: Record<string, unknown>
isInstalled?: boolean
installedAt?: Date
installedBy?: string
enabled?: boolean
config?: string | null
}
export interface PackageData {
packageId: string
data: string
}
export interface CreatePackageDataInput {
packageId: string
data: string
}
export interface UpdatePackageDataInput {
data?: string
}

View File

@@ -1,23 +1,41 @@
export type UserRole = 'public' | 'user' | 'moderator' | 'admin' | 'god' | 'supergod'
export interface User {
id: string
tenantId: string
username: string
email: string
role: 'user' | 'admin' | 'god' | 'supergod'
createdAt: Date
updatedAt: Date
role: UserRole
profilePicture?: string | null
bio?: string | null
createdAt: bigint
tenantId?: string | null
isInstanceOwner: boolean
passwordChangeTimestamp?: bigint | null
firstLogin: boolean
}
export interface CreateUserInput {
tenantId?: string
id?: string
username: string
email: string
role?: User['role']
role: UserRole
profilePicture?: string | null
bio?: string | null
createdAt?: bigint
tenantId?: string | null
isInstanceOwner?: boolean
passwordChangeTimestamp?: bigint | null
firstLogin?: boolean
}
export interface UpdateUserInput {
tenantId?: string
username?: string
email?: string
role?: User['role']
role?: UserRole
profilePicture?: string | null
bio?: string | null
tenantId?: string | null
isInstanceOwner?: boolean
passwordChangeTimestamp?: bigint | null
firstLogin?: boolean
}

View File

@@ -7,6 +7,7 @@ export { isValidTitle } from './validation/is-valid-title'
export { isValidLevel } from './validation/is-valid-level'
export { isValidUuid } from './validation/is-valid-uuid'
export { isPlainObject } from './validation/is-plain-object'
export { isValidJsonString } from './validation/is-valid-json'
export { validateUserCreate } from './validation/validate-user-create'
export { validateUserUpdate } from './validation/validate-user-update'
export { validateCredentialCreate } from './validation/validate-credential-create'

View File

@@ -0,0 +1,3 @@
import { isValidJsonString as isValidJsonStringPredicate } from '../../validation/predicates/string/is-valid-json'
export const isValidJsonString = (value: string): boolean => isValidJsonStringPredicate(value)

View File

@@ -1,38 +1,43 @@
/**
* @file types.ts
* @description Core DBAL types (stub)
* @description Core DBAL types
*/
export interface DBALError {
code: string;
message: string;
code: string
message: string
}
export interface Result<T> {
success: boolean;
data?: T;
error?: DBALError;
success: boolean
data?: T
error?: DBALError
}
export interface ListOptions {
filter?: Record<string, unknown>;
sort?: Record<string, 'asc' | 'desc'>;
page?: number;
limit?: number;
filter?: Record<string, unknown>
sort?: Record<string, 'asc' | 'desc'>
page?: number
limit?: number
}
export interface ListResult<T> {
items: T[];
total: number;
page: number;
limit: number;
hasMore: boolean;
items: T[]
total: number
page: number
limit: number
hasMore: boolean
}
export type User = any;
export type Session = any;
export type Page = any;
export type PageView = any;
export type Workflow = any;
export type LuaScript = any;
export type Component = any;
export type {
User,
Credential,
Session,
Workflow,
LuaScript,
PageConfig,
ComponentNode,
ComponentConfig,
InstalledPackage,
PackageData,
} from './foundation/types'

View File

@@ -1,26 +1,31 @@
import type { ComponentHierarchy } from '../types'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidUuid } from '../../predicates/is-valid-uuid'
import type { ComponentNode } from '../types'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validateComponentHierarchyCreate(data: Partial<ComponentHierarchy>): string[] {
export function validateComponentHierarchyCreate(data: Partial<ComponentNode>): string[] {
const errors: string[] = []
if (!data.pageId) {
errors.push('pageId is required')
} else if (!isValidUuid(data.pageId)) {
errors.push('pageId must be a valid UUID')
} else if (typeof data.pageId !== 'string' || data.pageId.trim().length === 0) {
errors.push('pageId must be a non-empty string')
}
if (data.parentId !== undefined) {
if (typeof data.parentId !== 'string' || !isValidUuid(data.parentId)) {
errors.push('parentId must be a valid UUID')
if (data.parentId !== null && (typeof data.parentId !== 'string' || data.parentId.trim().length === 0)) {
errors.push('parentId must be a non-empty string')
}
}
if (!data.componentType) {
errors.push('componentType is required')
} else if (data.componentType.length > 100) {
errors.push('componentType must be 1-100 characters')
if (!data.type) {
errors.push('type is required')
} else if (typeof data.type !== 'string' || data.type.length > 100) {
errors.push('type must be 1-100 characters')
}
if (!data.childIds) {
errors.push('childIds is required')
} else if (typeof data.childIds !== 'string' || !isValidJsonString(data.childIds)) {
errors.push('childIds must be a JSON string')
}
if (data.order === undefined) {
@@ -29,11 +34,5 @@ export function validateComponentHierarchyCreate(data: Partial<ComponentHierarch
errors.push('order must be a non-negative integer')
}
if (data.props === undefined) {
errors.push('props is required')
} else if (!isPlainObject(data.props)) {
errors.push('props must be an object')
}
return errors
}

View File

@@ -1,25 +1,30 @@
import type { ComponentHierarchy } from '../types'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidUuid } from '../../predicates/is-valid-uuid'
import type { ComponentNode } from '../types'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validateComponentHierarchyUpdate(data: Partial<ComponentHierarchy>): string[] {
export function validateComponentHierarchyUpdate(data: Partial<ComponentNode>): string[] {
const errors: string[] = []
if (data.pageId !== undefined) {
if (typeof data.pageId !== 'string' || !isValidUuid(data.pageId)) {
errors.push('pageId must be a valid UUID')
if (typeof data.pageId !== 'string' || data.pageId.trim().length === 0) {
errors.push('pageId must be a non-empty string')
}
}
if (data.parentId !== undefined) {
if (typeof data.parentId !== 'string' || !isValidUuid(data.parentId)) {
errors.push('parentId must be a valid UUID')
if (data.parentId !== null && (typeof data.parentId !== 'string' || data.parentId.trim().length === 0)) {
errors.push('parentId must be a non-empty string')
}
}
if (data.componentType !== undefined) {
if (typeof data.componentType !== 'string' || data.componentType.length === 0 || data.componentType.length > 100) {
errors.push('componentType must be 1-100 characters')
if (data.type !== undefined) {
if (typeof data.type !== 'string' || data.type.length === 0 || data.type.length > 100) {
errors.push('type must be 1-100 characters')
}
}
if (data.childIds !== undefined) {
if (typeof data.childIds !== 'string' || !isValidJsonString(data.childIds)) {
errors.push('childIds must be a JSON string')
}
}
@@ -29,9 +34,5 @@ export function validateComponentHierarchyUpdate(data: Partial<ComponentHierarch
}
}
if (data.props !== undefined && !isPlainObject(data.props)) {
errors.push('props must be an object')
}
return errors
}

View File

@@ -16,11 +16,5 @@ export function validateCredentialCreate(data: Partial<Credential>): string[] {
errors.push('passwordHash must be a non-empty string')
}
if (data.firstLogin === undefined) {
errors.push('firstLogin is required')
} else if (typeof data.firstLogin !== 'boolean') {
errors.push('firstLogin must be a boolean')
}
return errors
}

View File

@@ -16,9 +16,5 @@ export function validateCredentialUpdate(data: Partial<Credential>): string[] {
}
}
if (data.firstLogin !== undefined && typeof data.firstLogin !== 'boolean') {
errors.push('firstLogin must be a boolean')
}
return errors
}

View File

@@ -1,6 +1,6 @@
import type { LuaScript } from '../types'
import { isAllowedLuaGlobal } from '../../predicates/lua/is-allowed-lua-global'
import { isValidUuid } from '../../predicates/is-valid-uuid'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
const errors: string[] = []
@@ -17,6 +17,16 @@ export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
errors.push('code must be a non-empty string')
}
if (!data.parameters) {
errors.push('parameters is required')
} else if (typeof data.parameters !== 'string' || !isValidJsonString(data.parameters)) {
errors.push('parameters must be a JSON string')
}
if (data.returnType !== undefined && data.returnType !== null && typeof data.returnType !== 'string') {
errors.push('returnType must be a string')
}
if (data.isSandboxed === undefined) {
errors.push('isSandboxed is required')
} else if (typeof data.isSandboxed !== 'boolean') {
@@ -25,14 +35,23 @@ export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
if (data.allowedGlobals === undefined) {
errors.push('allowedGlobals is required')
} else if (!Array.isArray(data.allowedGlobals)) {
errors.push('allowedGlobals must be an array of strings')
} else if (data.allowedGlobals.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
errors.push('allowedGlobals must contain non-empty strings')
} else if (typeof data.allowedGlobals !== 'string' || !isValidJsonString(data.allowedGlobals)) {
errors.push('allowedGlobals must be a JSON string')
} else {
const invalidGlobals = data.allowedGlobals.filter((entry) => !isAllowedLuaGlobal(entry))
if (invalidGlobals.length > 0) {
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
try {
const parsed = JSON.parse(data.allowedGlobals) as unknown
if (!Array.isArray(parsed)) {
errors.push('allowedGlobals must be a JSON array')
} else if (parsed.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
errors.push('allowedGlobals must contain non-empty strings')
} else {
const invalidGlobals = parsed.filter((entry) => !isAllowedLuaGlobal(entry))
if (invalidGlobals.length > 0) {
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
}
}
} catch {
errors.push('allowedGlobals must be valid JSON')
}
}
@@ -42,10 +61,26 @@ export function validateLuaScriptCreate(data: Partial<LuaScript>): string[] {
errors.push('timeoutMs must be an integer between 100 and 30000')
}
if (!data.createdBy) {
errors.push('createdBy is required')
} else if (!isValidUuid(data.createdBy)) {
errors.push('createdBy must be a valid UUID')
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
errors.push('version must be a positive integer')
}
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
errors.push('updatedAt must be a bigint timestamp')
}
if (data.createdBy !== undefined && data.createdBy !== null) {
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
errors.push('createdBy must be a non-empty string')
}
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
if (data.description !== undefined && typeof data.description !== 'string') {

View File

@@ -1,6 +1,6 @@
import type { LuaScript } from '../types'
import { isAllowedLuaGlobal } from '../../predicates/lua/is-allowed-lua-global'
import { isValidUuid } from '../../predicates/is-valid-uuid'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validateLuaScriptUpdate(data: Partial<LuaScript>): string[] {
const errors: string[] = []
@@ -17,19 +17,38 @@ export function validateLuaScriptUpdate(data: Partial<LuaScript>): string[] {
}
}
if (data.parameters !== undefined) {
if (typeof data.parameters !== 'string' || !isValidJsonString(data.parameters)) {
errors.push('parameters must be a JSON string')
}
}
if (data.returnType !== undefined && data.returnType !== null && typeof data.returnType !== 'string') {
errors.push('returnType must be a string')
}
if (data.isSandboxed !== undefined && typeof data.isSandboxed !== 'boolean') {
errors.push('isSandboxed must be a boolean')
}
if (data.allowedGlobals !== undefined) {
if (!Array.isArray(data.allowedGlobals)) {
errors.push('allowedGlobals must be an array of strings')
} else if (data.allowedGlobals.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
errors.push('allowedGlobals must contain non-empty strings')
if (typeof data.allowedGlobals !== 'string' || !isValidJsonString(data.allowedGlobals)) {
errors.push('allowedGlobals must be a JSON string')
} else {
const invalidGlobals = data.allowedGlobals.filter((entry) => !isAllowedLuaGlobal(entry))
if (invalidGlobals.length > 0) {
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
try {
const parsed = JSON.parse(data.allowedGlobals) as unknown
if (!Array.isArray(parsed)) {
errors.push('allowedGlobals must be a JSON array')
} else if (parsed.some(entry => typeof entry !== 'string' || entry.trim().length === 0)) {
errors.push('allowedGlobals must contain non-empty strings')
} else {
const invalidGlobals = parsed.filter((entry) => !isAllowedLuaGlobal(entry))
if (invalidGlobals.length > 0) {
errors.push(`allowedGlobals contains forbidden globals: ${invalidGlobals.join(', ')}`)
}
}
} catch {
errors.push('allowedGlobals must be valid JSON')
}
}
}
@@ -40,12 +59,28 @@ export function validateLuaScriptUpdate(data: Partial<LuaScript>): string[] {
}
}
if (data.createdBy !== undefined) {
if (typeof data.createdBy !== 'string' || !isValidUuid(data.createdBy)) {
errors.push('createdBy must be a valid UUID')
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
errors.push('version must be a positive integer')
}
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
errors.push('updatedAt must be a bigint timestamp')
}
if (data.createdBy !== undefined && data.createdBy !== null) {
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
errors.push('createdBy must be a non-empty string')
}
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
if (data.description !== undefined && typeof data.description !== 'string') {
errors.push('description must be a string')
}

View File

@@ -1,16 +1,20 @@
import type { Package } from '../types'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidDate } from '../../predicates/is-valid-date'
import type { InstalledPackage } from '../types'
import { isValidSemver } from '../../predicates/string/is-valid-semver'
import { isValidUuid } from '../../predicates/is-valid-uuid'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validatePackageCreate(data: Partial<Package>): string[] {
export function validatePackageCreate(data: Partial<InstalledPackage>): string[] {
const errors: string[] = []
if (!data.name) {
errors.push('name is required')
} else if (typeof data.name !== 'string' || data.name.length > 255) {
errors.push('name must be 1-255 characters')
if (!data.packageId) {
errors.push('packageId is required')
} else if (typeof data.packageId !== 'string' || data.packageId.trim().length === 0) {
errors.push('packageId must be a non-empty string')
}
if (data.installedAt === undefined) {
errors.push('installedAt is required')
} else if (typeof data.installedAt !== 'bigint') {
errors.push('installedAt must be a bigint timestamp')
}
if (!data.version) {
@@ -19,36 +23,20 @@ export function validatePackageCreate(data: Partial<Package>): string[] {
errors.push('version must be semantic (x.y.z)')
}
if (!data.author) {
errors.push('author is required')
} else if (typeof data.author !== 'string' || data.author.length > 255) {
errors.push('author must be 1-255 characters')
if (data.enabled === undefined) {
errors.push('enabled is required')
} else if (typeof data.enabled !== 'boolean') {
errors.push('enabled must be a boolean')
}
if (data.manifest === undefined) {
errors.push('manifest is required')
} else if (!isPlainObject(data.manifest)) {
errors.push('manifest must be an object')
}
if (data.isInstalled === undefined) {
errors.push('isInstalled is required')
} else if (typeof data.isInstalled !== 'boolean') {
errors.push('isInstalled must be a boolean')
}
if (data.installedAt !== undefined && !isValidDate(data.installedAt)) {
errors.push('installedAt must be a valid date')
}
if (data.installedBy !== undefined) {
if (typeof data.installedBy !== 'string' || !isValidUuid(data.installedBy)) {
errors.push('installedBy must be a valid UUID')
if (data.config !== undefined && data.config !== null) {
if (typeof data.config !== 'string' || !isValidJsonString(data.config)) {
errors.push('config must be a JSON string')
}
}
if (data.description !== undefined && typeof data.description !== 'string') {
errors.push('description must be a string')
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
return errors

View File

@@ -1,16 +1,12 @@
import type { Package } from '../types'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidDate } from '../../predicates/is-valid-date'
import type { InstalledPackage } from '../types'
import { isValidSemver } from '../../predicates/string/is-valid-semver'
import { isValidUuid } from '../../predicates/is-valid-uuid'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validatePackageUpdate(data: Partial<Package>): string[] {
export function validatePackageUpdate(data: Partial<InstalledPackage>): string[] {
const errors: string[] = []
if (data.name !== undefined) {
if (typeof data.name !== 'string' || data.name.length === 0 || data.name.length > 255) {
errors.push('name must be 1-255 characters')
}
if (data.installedAt !== undefined && typeof data.installedAt !== 'bigint') {
errors.push('installedAt must be a bigint timestamp')
}
if (data.version !== undefined) {
@@ -19,32 +15,18 @@ export function validatePackageUpdate(data: Partial<Package>): string[] {
}
}
if (data.author !== undefined) {
if (typeof data.author !== 'string' || data.author.length === 0 || data.author.length > 255) {
errors.push('author must be 1-255 characters')
if (data.enabled !== undefined && typeof data.enabled !== 'boolean') {
errors.push('enabled must be a boolean')
}
if (data.config !== undefined && data.config !== null) {
if (typeof data.config !== 'string' || !isValidJsonString(data.config)) {
errors.push('config must be a JSON string')
}
}
if (data.manifest !== undefined && !isPlainObject(data.manifest)) {
errors.push('manifest must be an object')
}
if (data.isInstalled !== undefined && typeof data.isInstalled !== 'boolean') {
errors.push('isInstalled must be a boolean')
}
if (data.installedAt !== undefined && !isValidDate(data.installedAt)) {
errors.push('installedAt must be a valid date')
}
if (data.installedBy !== undefined) {
if (typeof data.installedBy !== 'string' || !isValidUuid(data.installedBy)) {
errors.push('installedBy must be a valid UUID')
}
}
if (data.description !== undefined && typeof data.description !== 'string') {
errors.push('description must be a string')
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
return errors

View File

@@ -1,19 +1,18 @@
import type { PageView } from '../types'
import type { PageConfig } from '../types'
import { isValidLevel } from '../../predicates/is-valid-level'
import { isValidSlug } from '../../predicates/string/is-valid-slug'
import { isValidTitle } from '../../predicates/string/is-valid-title'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
/**
* Validation rules for PageView entity
* Validation rules for PageConfig entity
*/
export function validatePageCreate(data: Partial<PageView>): string[] {
export function validatePageCreate(data: Partial<PageConfig>): string[] {
const errors: string[] = []
if (!data.slug) {
errors.push('Slug is required')
} else if (!isValidSlug(data.slug)) {
errors.push('Invalid slug format (lowercase alphanumeric, hyphen, slash, 1-255 chars)')
if (!data.path) {
errors.push('path is required')
} else if (typeof data.path !== 'string' || data.path.trim().length === 0 || data.path.length > 255) {
errors.push('path must be 1-255 characters')
}
if (!data.title) {
@@ -22,27 +21,79 @@ export function validatePageCreate(data: Partial<PageView>): string[] {
errors.push('Invalid title (must be 1-255 characters)')
}
if (!data.componentTree) {
errors.push('componentTree is required')
} else if (typeof data.componentTree !== 'string' || !isValidJsonString(data.componentTree)) {
errors.push('componentTree must be a JSON string')
}
if (data.level === undefined) {
errors.push('Level is required')
errors.push('level is required')
} else if (!isValidLevel(data.level)) {
errors.push('Invalid level (must be 1-5)')
errors.push('level must be between 1 and 6')
}
if (data.layout === undefined) {
errors.push('Layout is required')
} else if (!isPlainObject(data.layout)) {
errors.push('Layout must be an object')
if (data.requiresAuth === undefined) {
errors.push('requiresAuth is required')
} else if (typeof data.requiresAuth !== 'boolean') {
errors.push('requiresAuth must be a boolean')
}
if (data.isActive === undefined) {
errors.push('isActive is required')
} else if (typeof data.isActive !== 'boolean') {
errors.push('isActive must be a boolean')
if (data.isPublished !== undefined && typeof data.isPublished !== 'boolean') {
errors.push('isPublished must be a boolean')
}
if (data.description !== undefined && typeof data.description !== 'string') {
if (data.sortOrder !== undefined && (!Number.isInteger(data.sortOrder) || data.sortOrder < 0)) {
errors.push('sortOrder must be a non-negative integer')
}
if (data.params !== undefined && data.params !== null) {
if (typeof data.params !== 'string' || !isValidJsonString(data.params)) {
errors.push('params must be a JSON string')
}
}
if (data.meta !== undefined && data.meta !== null) {
if (typeof data.meta !== 'string' || !isValidJsonString(data.meta)) {
errors.push('meta must be a JSON string')
}
}
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
errors.push('updatedAt must be a bigint timestamp')
}
if (data.description !== undefined && data.description !== null && typeof data.description !== 'string') {
errors.push('Description must be a string')
}
if (data.icon !== undefined && data.icon !== null && typeof data.icon !== 'string') {
errors.push('icon must be a string')
}
if (data.component !== undefined && data.component !== null && typeof data.component !== 'string') {
errors.push('component must be a string')
}
if (data.requiredRole !== undefined && data.requiredRole !== null && typeof data.requiredRole !== 'string') {
errors.push('requiredRole must be a string')
}
if (data.parentPath !== undefined && data.parentPath !== null && typeof data.parentPath !== 'string') {
errors.push('parentPath must be a string')
}
if (data.packageId !== undefined && data.packageId !== null && typeof data.packageId !== 'string') {
errors.push('packageId must be a string')
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
return errors
}

View File

@@ -1,35 +1,90 @@
import type { PageView } from '../types'
import type { PageConfig } from '../types'
import { isValidLevel } from '../../predicates/is-valid-level'
import { isValidSlug } from '../../predicates/string/is-valid-slug'
import { isValidTitle } from '../../predicates/string/is-valid-title'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validatePageUpdate(data: Partial<PageView>): string[] {
export function validatePageUpdate(data: Partial<PageConfig>): string[] {
const errors: string[] = []
if (data.slug !== undefined && !isValidSlug(data.slug)) {
errors.push('Invalid slug format (lowercase alphanumeric, hyphen, slash, 1-255 chars)')
if (data.path !== undefined) {
if (typeof data.path !== 'string' || data.path.trim().length === 0 || data.path.length > 255) {
errors.push('path must be 1-255 characters')
}
}
if (data.title !== undefined && !isValidTitle(data.title)) {
errors.push('Invalid title (must be 1-255 characters)')
}
if (data.componentTree !== undefined) {
if (typeof data.componentTree !== 'string' || !isValidJsonString(data.componentTree)) {
errors.push('componentTree must be a JSON string')
}
}
if (data.level !== undefined && !isValidLevel(data.level)) {
errors.push('Invalid level (must be 1-5)')
errors.push('level must be between 1 and 6')
}
if (data.layout !== undefined && !isPlainObject(data.layout)) {
errors.push('Layout must be an object')
if (data.requiresAuth !== undefined && typeof data.requiresAuth !== 'boolean') {
errors.push('requiresAuth must be a boolean')
}
if (data.isActive !== undefined && typeof data.isActive !== 'boolean') {
errors.push('isActive must be a boolean')
if (data.isPublished !== undefined && typeof data.isPublished !== 'boolean') {
errors.push('isPublished must be a boolean')
}
if (data.description !== undefined && typeof data.description !== 'string') {
if (data.sortOrder !== undefined && (!Number.isInteger(data.sortOrder) || data.sortOrder < 0)) {
errors.push('sortOrder must be a non-negative integer')
}
if (data.params !== undefined && data.params !== null) {
if (typeof data.params !== 'string' || !isValidJsonString(data.params)) {
errors.push('params must be a JSON string')
}
}
if (data.meta !== undefined && data.meta !== null) {
if (typeof data.meta !== 'string' || !isValidJsonString(data.meta)) {
errors.push('meta must be a JSON string')
}
}
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
errors.push('updatedAt must be a bigint timestamp')
}
if (data.description !== undefined && data.description !== null && typeof data.description !== 'string') {
errors.push('Description must be a string')
}
if (data.icon !== undefined && data.icon !== null && typeof data.icon !== 'string') {
errors.push('icon must be a string')
}
if (data.component !== undefined && data.component !== null && typeof data.component !== 'string') {
errors.push('component must be a string')
}
if (data.requiredRole !== undefined && data.requiredRole !== null && typeof data.requiredRole !== 'string') {
errors.push('requiredRole must be a string')
}
if (data.parentPath !== undefined && data.parentPath !== null && typeof data.parentPath !== 'string') {
errors.push('parentPath must be a string')
}
if (data.packageId !== undefined && data.packageId !== null && typeof data.packageId !== 'string') {
errors.push('packageId must be a string')
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
return errors
}

View File

@@ -1,14 +1,11 @@
import type { Session } from '../types'
import { isValidDate } from '../../predicates/is-valid-date'
import { isValidUuid } from '../../predicates/is-valid-uuid'
export function validateSessionCreate(data: Partial<Session>): string[] {
const errors: string[] = []
if (!data.userId) {
errors.push('userId is required')
} else if (!isValidUuid(data.userId)) {
errors.push('userId must be a valid UUID')
} else if (typeof data.userId !== 'string' || data.userId.trim().length === 0) {
errors.push('userId must be a non-empty string')
}
if (!data.token) {
@@ -19,8 +16,24 @@ export function validateSessionCreate(data: Partial<Session>): string[] {
if (data.expiresAt === undefined) {
errors.push('expiresAt is required')
} else if (!isValidDate(data.expiresAt)) {
errors.push('expiresAt must be a valid date')
} else if (typeof data.expiresAt !== 'bigint') {
errors.push('expiresAt must be a bigint timestamp')
}
if (data.createdAt !== undefined && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.lastActivity !== undefined && typeof data.lastActivity !== 'bigint') {
errors.push('lastActivity must be a bigint timestamp')
}
if (data.ipAddress !== undefined && data.ipAddress !== null && typeof data.ipAddress !== 'string') {
errors.push('ipAddress must be a string')
}
if (data.userAgent !== undefined && data.userAgent !== null && typeof data.userAgent !== 'string') {
errors.push('userAgent must be a string')
}
return errors

View File

@@ -1,13 +1,10 @@
import type { Session } from '../types'
import { isValidDate } from '../../predicates/is-valid-date'
import { isValidUuid } from '../../predicates/is-valid-uuid'
export function validateSessionUpdate(data: Partial<Session>): string[] {
const errors: string[] = []
if (data.userId !== undefined) {
if (typeof data.userId !== 'string' || !isValidUuid(data.userId)) {
errors.push('userId must be a valid UUID')
if (typeof data.userId !== 'string' || data.userId.trim().length === 0) {
errors.push('userId must be a non-empty string')
}
}
@@ -17,12 +14,20 @@ export function validateSessionUpdate(data: Partial<Session>): string[] {
}
}
if (data.expiresAt !== undefined && !isValidDate(data.expiresAt)) {
errors.push('expiresAt must be a valid date')
if (data.expiresAt !== undefined && typeof data.expiresAt !== 'bigint') {
errors.push('expiresAt must be a bigint timestamp')
}
if (data.lastActivity !== undefined && !isValidDate(data.lastActivity)) {
errors.push('lastActivity must be a valid date')
if (data.lastActivity !== undefined && typeof data.lastActivity !== 'bigint') {
errors.push('lastActivity must be a bigint timestamp')
}
if (data.ipAddress !== undefined && data.ipAddress !== null && typeof data.ipAddress !== 'string') {
errors.push('ipAddress must be a string')
}
if (data.userAgent !== undefined && data.userAgent !== null && typeof data.userAgent !== 'string') {
errors.push('userAgent must be a string')
}
return errors

View File

@@ -1,19 +1,23 @@
/**
* @file types.ts
* @description Entity validation types (stub)
* @description Entity validation types
*/
import type {
User,
Credential,
Session,
PageConfig,
ComponentNode,
Workflow,
LuaScript,
InstalledPackage,
PackageData,
} from '../../foundation/types'
export interface ValidationResult {
isValid: boolean;
errors: string[];
isValid: boolean
errors: string[]
}
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;
export type Package = any;
export type { User, Credential, Session, PageConfig, ComponentNode, Workflow, LuaScript, InstalledPackage, PackageData }

View File

@@ -22,9 +22,37 @@ export function validateUserCreate(data: Partial<User>): string[] {
if (!data.role) {
errors.push('Role is required')
} else if (!['user', 'admin', 'god', 'supergod'].includes(data.role)) {
} else if (!['public', 'user', 'moderator', 'admin', 'god', 'supergod'].includes(data.role)) {
errors.push('Invalid role')
}
if (data.profilePicture !== undefined && data.profilePicture !== null && typeof data.profilePicture !== 'string') {
errors.push('profilePicture must be a string')
}
if (data.bio !== undefined && data.bio !== null && typeof data.bio !== 'string') {
errors.push('bio must be a string')
}
if (data.createdAt !== undefined && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
if (data.isInstanceOwner !== undefined && typeof data.isInstanceOwner !== 'boolean') {
errors.push('isInstanceOwner must be a boolean')
}
if (data.passwordChangeTimestamp !== undefined && data.passwordChangeTimestamp !== null && typeof data.passwordChangeTimestamp !== 'bigint') {
errors.push('passwordChangeTimestamp must be a bigint timestamp')
}
if (data.firstLogin !== undefined && typeof data.firstLogin !== 'boolean') {
errors.push('firstLogin must be a boolean')
}
return errors
}

View File

@@ -13,9 +13,37 @@ export function validateUserUpdate(data: Partial<User>): string[] {
errors.push('Invalid email format (max 255 chars)')
}
if (data.role !== undefined && !['user', 'admin', 'god', 'supergod'].includes(data.role)) {
if (data.role !== undefined && !['public', 'user', 'moderator', 'admin', 'god', 'supergod'].includes(data.role)) {
errors.push('Invalid role')
}
if (data.profilePicture !== undefined && data.profilePicture !== null && typeof data.profilePicture !== 'string') {
errors.push('profilePicture must be a string')
}
if (data.bio !== undefined && data.bio !== null && typeof data.bio !== 'string') {
errors.push('bio must be a string')
}
if (data.createdAt !== undefined && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
if (data.isInstanceOwner !== undefined && typeof data.isInstanceOwner !== 'boolean') {
errors.push('isInstanceOwner must be a boolean')
}
if (data.passwordChangeTimestamp !== undefined && data.passwordChangeTimestamp !== null && typeof data.passwordChangeTimestamp !== 'bigint') {
errors.push('passwordChangeTimestamp must be a bigint timestamp')
}
if (data.firstLogin !== undefined && typeof data.firstLogin !== 'boolean') {
errors.push('firstLogin must be a boolean')
}
return errors
}

View File

@@ -1,5 +1,3 @@
import { isValidUuid } from '../predicates/is-valid-uuid'
export function validateId(id: string): string[] {
const errors: string[] = []
@@ -8,9 +6,5 @@ export function validateId(id: string): string[] {
return errors
}
if (!isValidUuid(id.trim())) {
errors.push('ID must be a valid UUID')
}
return errors
}

View File

@@ -1,8 +1,5 @@
import type { Workflow } from '../types'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidUuid } from '../../predicates/is-valid-uuid'
const triggerValues = ['manual', 'schedule', 'event', 'webhook'] as const
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validateWorkflowCreate(data: Partial<Workflow>): string[] {
const errors: string[] = []
@@ -13,34 +10,44 @@ export function validateWorkflowCreate(data: Partial<Workflow>): string[] {
errors.push('name must be 1-255 characters')
}
if (!data.trigger) {
errors.push('trigger is required')
} else if (!triggerValues.includes(data.trigger)) {
errors.push('trigger must be one of manual, schedule, event, webhook')
if (!data.nodes) {
errors.push('nodes is required')
} else if (typeof data.nodes !== 'string' || !isValidJsonString(data.nodes)) {
errors.push('nodes must be a JSON string')
}
if (data.triggerConfig === undefined) {
errors.push('triggerConfig is required')
} else if (!isPlainObject(data.triggerConfig)) {
errors.push('triggerConfig must be an object')
if (!data.edges) {
errors.push('edges is required')
} else if (typeof data.edges !== 'string' || !isValidJsonString(data.edges)) {
errors.push('edges must be a JSON string')
}
if (data.steps === undefined) {
errors.push('steps is required')
} else if (!isPlainObject(data.steps)) {
errors.push('steps must be an object')
if (data.enabled === undefined) {
errors.push('enabled is required')
} else if (typeof data.enabled !== 'boolean') {
errors.push('enabled must be a boolean')
}
if (data.isActive === undefined) {
errors.push('isActive is required')
} else if (typeof data.isActive !== 'boolean') {
errors.push('isActive must be a boolean')
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
errors.push('version must be a positive integer')
}
if (!data.createdBy) {
errors.push('createdBy is required')
} else if (!isValidUuid(data.createdBy)) {
errors.push('createdBy must be a valid UUID')
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
errors.push('updatedAt must be a bigint timestamp')
}
if (data.createdBy !== undefined && data.createdBy !== null) {
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
errors.push('createdBy must be a non-empty string')
}
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
if (data.description !== undefined && typeof data.description !== 'string') {

View File

@@ -1,8 +1,5 @@
import type { Workflow } from '../types'
import { isPlainObject } from '../../predicates/is-plain-object'
import { isValidUuid } from '../../predicates/is-valid-uuid'
const triggerValues = ['manual', 'schedule', 'event', 'webhook'] as const
import { isValidJsonString } from '../../predicates/string/is-valid-json'
export function validateWorkflowUpdate(data: Partial<Workflow>): string[] {
const errors: string[] = []
@@ -13,28 +10,44 @@ export function validateWorkflowUpdate(data: Partial<Workflow>): string[] {
}
}
if (data.trigger !== undefined && !triggerValues.includes(data.trigger)) {
errors.push('trigger must be one of manual, schedule, event, webhook')
}
if (data.triggerConfig !== undefined && !isPlainObject(data.triggerConfig)) {
errors.push('triggerConfig must be an object')
}
if (data.steps !== undefined && !isPlainObject(data.steps)) {
errors.push('steps must be an object')
}
if (data.isActive !== undefined && typeof data.isActive !== 'boolean') {
errors.push('isActive must be a boolean')
}
if (data.createdBy !== undefined) {
if (typeof data.createdBy !== 'string' || !isValidUuid(data.createdBy)) {
errors.push('createdBy must be a valid UUID')
if (data.nodes !== undefined) {
if (typeof data.nodes !== 'string' || !isValidJsonString(data.nodes)) {
errors.push('nodes must be a JSON string')
}
}
if (data.edges !== undefined) {
if (typeof data.edges !== 'string' || !isValidJsonString(data.edges)) {
errors.push('edges must be a JSON string')
}
}
if (data.enabled !== undefined && typeof data.enabled !== 'boolean') {
errors.push('enabled must be a boolean')
}
if (data.version !== undefined && (!Number.isInteger(data.version) || data.version < 1)) {
errors.push('version must be a positive integer')
}
if (data.createdAt !== undefined && data.createdAt !== null && typeof data.createdAt !== 'bigint') {
errors.push('createdAt must be a bigint timestamp')
}
if (data.updatedAt !== undefined && data.updatedAt !== null && typeof data.updatedAt !== 'bigint') {
errors.push('updatedAt must be a bigint timestamp')
}
if (data.createdBy !== undefined && data.createdBy !== null) {
if (typeof data.createdBy !== 'string' || data.createdBy.trim().length === 0) {
errors.push('createdBy must be a non-empty string')
}
}
if (data.tenantId !== undefined && data.tenantId !== null && typeof data.tenantId !== 'string') {
errors.push('tenantId must be a string')
}
if (data.description !== undefined && typeof data.description !== 'string') {
errors.push('description must be a string')
}

View File

@@ -1,4 +1,4 @@
// Level validation: 1-5 range
// Level validation: 1-6 range
export function isValidLevel(level: number): boolean {
return Number.isInteger(level) && level >= 1 && level <= 5
return Number.isInteger(level) && level >= 1 && level <= 6
}

View File

@@ -0,0 +1,9 @@
export const isValidJsonString = (value: string): boolean => {
if (typeof value !== 'string') return false
try {
JSON.parse(value)
return true
} catch {
return false
}
}

View File

@@ -13,30 +13,27 @@ interface PackageStyleLoaderProps {
packages: string[]
}
export function PackageStyleLoader({ packages }: PackageStyleLoaderProps) {
export function PackageStyleLoader({ packages }: PackageStyleLoaderProps): null {
useEffect(() => {
async function loadStyles() {
// eslint-disable-next-line no-console
console.log(`📦 Loading styles for ${packages.length} packages...`)
async function loadStyles(): Promise<void> {
const results = await Promise.all(
packages.map(async (packageId) => {
try {
const css = await loadAndInjectStyles(packageId)
// eslint-disable-next-line no-console
console.log(`${packageId} (${css.length} bytes)`)
return { packageId, success: true, size: css.length }
} catch (error) {
console.warn(`${packageId}:`, error)
} catch {
return { packageId, success: false, size: 0 }
}
})
)
const successful = results.filter(r => r.success)
const totalSize = successful.reduce((sum, r) => sum + r.size, 0)
// eslint-disable-next-line no-console
console.log(`${successful.length}/${packages.length} packages loaded (${(totalSize / 1024).toFixed(1)}KB)`)
// Log summary in development only
if (process.env.NODE_ENV === 'development') {
const successful = results.filter(r => r.success)
const totalSize = successful.reduce((sum, r) => sum + r.size, 0)
// eslint-disable-next-line no-console
console.log(`[PackageStyleLoader] ${successful.length}/${packages.length} packages (${(totalSize / 1024).toFixed(1)}KB)`)
}
}
if (packages.length > 0) {

View File

@@ -1,25 +1,58 @@
/**
* Hook for level-based routing functionality
*
* Provides permission checking and routing based on the 6-level system:
* 0: public, 1: user, 2: moderator, 3: admin, 4: god, 5: supergod
*/
import { useRouter } from 'next/navigation'
import { useResolvedUser } from './useResolvedUser'
export interface LevelRouting {
canAccessLevel: (level: number) => boolean
redirectToLevel: (level: number) => void
/** Check if current user can access a given permission level */
canAccessLevel: (requiredLevel: number) => boolean
/** Redirect user to an appropriate page for their level */
redirectToLevel: (targetLevel: number) => void
/** Current user's permission level */
currentLevel: number
/** Whether the user check is still loading */
isLoading: boolean
}
/** Route mappings for each permission level */
const LEVEL_ROUTES: Record<number, string> = {
0: '/', // Public home
1: '/dashboard', // User dashboard
2: '/moderate', // Moderator panel
3: '/admin', // Admin panel
4: '/god', // God panel
5: '/supergod', // Supergod panel
}
/**
* Hook for managing level-based routing
* TODO: Implement full level routing logic
* Uses the resolved user state to check permissions.
*/
export function useLevelRouting(): LevelRouting {
const router = useRouter()
const { level, isLoading } = useResolvedUser()
const canAccessLevel = (requiredLevel: number): boolean => {
if (isLoading) {
return false // Don't grant access while loading
}
return level >= requiredLevel
}
const redirectToLevel = (targetLevel: number): void => {
const route = LEVEL_ROUTES[targetLevel] ?? LEVEL_ROUTES[0]
router.push(route)
}
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
},
canAccessLevel,
redirectToLevel,
currentLevel: level,
isLoading,
}
}

View File

@@ -1,22 +1,55 @@
/**
* Hook for resolved user state
*
* Provides user information from the auth system with level-based permissions.
* Use this hook when you need to check user permissions or identity.
*/
import { useAuth } from '../useAuth'
export interface ResolvedUserState {
userId?: string
username?: string
level?: number
email?: string
role?: string
level: number
tenantId?: string
isAuthenticated: boolean
isLoading: boolean
error?: string
}
/**
* Hook for managing resolved user state
* TODO: Implement full user resolution logic
* Returns user data from session with computed permission level.
*/
export function useResolvedUser(): ResolvedUserState {
// TODO: Implement user resolution from session/auth
const { user, isAuthenticated, isLoading } = useAuth()
if (isLoading) {
return {
level: 0,
isAuthenticated: false,
isLoading: true,
}
}
if (user === null || !isAuthenticated) {
return {
level: 0,
isAuthenticated: false,
isLoading: false,
}
}
return {
userId: user.id,
username: user.username,
email: user.email,
role: user.role,
level: user.level ?? 0,
tenantId: user.tenantId,
isAuthenticated: true,
isLoading: false,
}
}

View File

@@ -2,7 +2,7 @@
* useCodeEditor hook
*/
import { useState, useCallback } from 'react'
import { useState, useCallback, useRef } from 'react'
export interface EditorFile {
path: string
@@ -13,6 +13,10 @@ export interface EditorFile {
export function useCodeEditor() {
const [files, setFiles] = useState<EditorFile[]>([])
const [currentFile, setCurrentFile] = useState<EditorFile | null>(null)
// Use ref to avoid stale closures in callbacks
const currentFileRef = useRef<EditorFile | null>(null)
currentFileRef.current = currentFile
const openFile = useCallback((file: EditorFile) => {
setFiles(prev => {
@@ -29,17 +33,17 @@ export function useCodeEditor() {
const saveFile = useCallback((file: EditorFile) => {
setFiles(prev => prev.map(f => f.path === file.path ? file : f))
if (currentFile?.path === file.path) {
if (currentFileRef.current?.path === file.path) {
setCurrentFile(file)
}
}, [currentFile])
}, [])
const closeFile = useCallback((path: string) => {
setFiles(prev => prev.filter(f => f.path !== path))
if (currentFile?.path === path) {
if (currentFileRef.current?.path === path) {
setCurrentFile(null)
}
}, [currentFile])
}, [])
return {
files,

View File

@@ -1,20 +1,20 @@
import { seedAppConfig } from './app/seed-app-config'
import { seedCssCategories } from './css/seed-css-categories'
import { seedDropdownConfigs } from './dropdowns/seed-dropdown-configs'
import { seedUsers } from './users/seed-users'
/**
* Seed database with default data
*/
export const seedDefaultData = async (): Promise<void> => {
// TODO: Implement seedUsers function and import it
// await seedUsers()
await seedUsers()
await seedAppConfig()
await seedCssCategories()
await seedDropdownConfigs()
}
export const defaultDataBuilders = {
// seedUsers,
seedUsers,
seedAppConfig,
seedCssCategories,
seedDropdownConfigs,

View File

@@ -0,0 +1,84 @@
import { getAdapter } from '../../../core/dbal-client'
import { hashPassword } from '../../../password/hash-password'
/**
* Default users for initial system setup.
* In production, change these passwords immediately after first login.
*/
const DEFAULT_USERS = [
{
id: 'user_supergod',
username: 'admin',
email: 'admin@localhost',
role: 'supergod',
isInstanceOwner: true,
password: 'admin123', // Change immediately in production!
},
{
id: 'user_god',
username: 'god',
email: 'god@localhost',
role: 'god',
isInstanceOwner: false,
password: 'god123',
},
{
id: 'user_admin',
username: 'manager',
email: 'manager@localhost',
role: 'admin',
isInstanceOwner: false,
password: 'manager123',
},
{
id: 'user_demo',
username: 'demo',
email: 'demo@localhost',
role: 'user',
isInstanceOwner: false,
password: 'demo123',
},
]
/**
* Seed default users and their credentials.
* Creates users only if they don't already exist.
*/
export async function seedUsers(): Promise<void> {
const adapter = getAdapter()
const now = BigInt(Date.now())
for (const userData of DEFAULT_USERS) {
// Check if user already exists
const existing = await adapter.findFirst('User', {
where: { username: userData.username },
})
if (existing !== null && existing !== undefined) {
// User already exists, skip
continue
}
// Create user
await adapter.create('User', {
id: userData.id,
username: userData.username,
email: userData.email,
role: userData.role,
isInstanceOwner: userData.isInstanceOwner,
createdAt: now,
tenantId: null,
profilePicture: null,
bio: null,
})
// Create credential for login
const passwordHash = await hashPassword(userData.password)
await adapter.create('Credential', {
id: `cred_${userData.id}`,
username: userData.username,
passwordHash,
userId: userData.id,
})
}
}

View File

@@ -32,6 +32,8 @@ interface RequestOptions {
orderBy?: Record<string, 'asc' | 'desc'>
/** Override the package for this request (useful for dependency packages) */
packageId?: string
/** AbortSignal for cancelling the request */
signal?: AbortSignal
}
/**
@@ -104,9 +106,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
setError(null)
try {
const { packageId: pkgOverride, ...queryOpts } = options ?? {}
const { packageId: pkgOverride, signal, ...queryOpts } = options ?? {}
const url = buildUrl(entity, undefined, undefined, pkgOverride) + buildQueryString(queryOpts as RequestOptions)
const response = await fetch(url)
const response = await fetch(url, { signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
@@ -120,6 +122,10 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
return json.data ?? []
} catch (err) {
// Don't set error state for aborted requests
if (err instanceof Error && err.name === 'AbortError') {
throw err
}
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
throw err
@@ -134,13 +140,13 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
* Read single entity
*/
const read = useCallback(
async (entity: string, id: string): Promise<T | null> => {
async (entity: string, id: string, options?: { signal?: AbortSignal }): Promise<T | null> => {
setLoading(true)
setError(null)
try {
const url = buildUrl(entity, id)
const response = await fetch(url)
const response = await fetch(url, { signal: options?.signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
@@ -154,6 +160,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
return json.data ?? null
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
throw err
}
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
throw err
@@ -168,7 +177,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
* Create entity
*/
const create = useCallback(
async (entity: string, data: Record<string, unknown>): Promise<T> => {
async (entity: string, data: Record<string, unknown>, options?: { signal?: AbortSignal }): Promise<T> => {
setLoading(true)
setError(null)
@@ -178,6 +187,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
signal: options?.signal,
})
if (!response.ok) {
@@ -192,6 +202,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
return json.data as T
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
throw err
}
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
throw err
@@ -206,7 +219,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
* Update entity
*/
const update = useCallback(
async (entity: string, id: string, data: Record<string, unknown>): Promise<T> => {
async (entity: string, id: string, data: Record<string, unknown>, options?: { signal?: AbortSignal }): Promise<T> => {
setLoading(true)
setError(null)
@@ -216,6 +229,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
signal: options?.signal,
})
if (!response.ok) {
@@ -230,6 +244,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
return json.data as T
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
throw err
}
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
throw err
@@ -244,13 +261,13 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
* Delete entity
*/
const remove = useCallback(
async (entity: string, id: string): Promise<void> => {
async (entity: string, id: string, options?: { signal?: AbortSignal }): Promise<void> => {
setLoading(true)
setError(null)
try {
const url = buildUrl(entity, id)
const response = await fetch(url, { method: 'DELETE' })
const response = await fetch(url, { method: 'DELETE', signal: options?.signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
@@ -262,6 +279,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
throw new Error(json.error ?? 'Request failed')
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
throw err
}
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
throw err
@@ -280,7 +300,8 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
entity: string,
id: string,
actionName: string,
data?: Record<string, unknown>
data?: Record<string, unknown>,
options?: { signal?: AbortSignal }
): Promise<T> => {
setLoading(true)
setError(null)
@@ -291,6 +312,7 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: data !== undefined ? JSON.stringify(data) : undefined,
signal: options?.signal,
})
if (!response.ok) {
@@ -305,6 +327,9 @@ export function useRestApi<T = unknown>(options?: UseRestApiOptions) {
return json.data as T
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
throw err
}
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
throw err

View File

@@ -1,16 +0,0 @@
/**
* 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: unknown): ReactNode {
// TODO: Implement Lua component tree generation
return null
}

View File

@@ -0,0 +1,100 @@
/**
* Generate React component tree from Lua UI component definitions
*
* Transforms LuaUIComponent structures into React elements using
* the JSON component renderer infrastructure.
*/
import React, { type ReactNode } from 'react'
import type { LuaUIComponent } from './types/lua-ui-package'
export interface ComponentTree {
type: string
props?: Record<string, unknown>
children?: ComponentTree[]
}
/**
* Map of basic HTML elements that can be rendered directly
*/
const HTML_ELEMENTS = new Set([
'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'section', 'article', 'header', 'footer', 'main', 'aside', 'nav',
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td',
'form', 'input', 'button', 'select', 'option', 'textarea', 'label',
'a', 'img', 'video', 'audio', 'canvas', 'svg',
'strong', 'em', 'code', 'pre', 'blockquote',
])
/**
* Render a single LuaUIComponent to a React element
*/
function renderComponent(component: LuaUIComponent, key?: string | number): ReactNode {
const { type, props = {}, children } = component
// Render children recursively
const renderedChildren = children?.map((child, index) =>
renderComponent(child, index)
)
// Handle text content
if (type === 'text' && typeof props.content === 'string') {
return props.content
}
// Handle HTML elements
if (HTML_ELEMENTS.has(type)) {
return React.createElement(
type,
{ ...props, key },
renderedChildren
)
}
// Handle custom components - wrap in div with data attribute for future component registry
return React.createElement(
'div',
{
'data-component': type,
className: `component-${type}`,
...props,
key,
},
renderedChildren ?? (
<span className="component-placeholder">
[{type}]
</span>
)
)
}
/**
* Generate a complete React component tree from a Lua UI component definition.
*
* @param luaComponent - The root LuaUIComponent to render
* @returns React node tree, or null if input is invalid
*/
export function generateComponentTree(luaComponent: unknown): ReactNode {
// Validate input
if (luaComponent === null || luaComponent === undefined) {
return null
}
if (typeof luaComponent !== 'object') {
return null
}
const component = luaComponent as LuaUIComponent
// Must have a type to render
if (typeof component.type !== 'string' || component.type.length === 0) {
return (
<div className="component-error">
Invalid component: missing type
</div>
)
}
return renderComponent(component)
}